56 Commits

Author SHA1 Message Date
2258ae53fe fix: avoid implicit clang target on native builds 2026-10-21 21:50:42 +08:00
0f2182fe96 Merge origin/master into master 2026-10-21 20:50:46 +08:00
0f456b4201 feat: complete structured CLI output
- unify human, plain, and JSON CLI output modes

- standardize diagnostics, lifecycle events, colors, quiet, and verbose handling

- preserve argument forwarding and improve CLI help and summaries

- document and release version 0.9.5
2026-10-21 18:55:23 +08: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
b0e426c189 Merge pull request #32 from dexoron/dev
feat: add package.dcr-version pin with load-time compatibility warnings
2026-08-08 15:00:02 +08:00
60d7420929 feat: add package.dcr-version pin with load-time compatibility warnings 2026-08-08 14:33:46 +08:00
3bd91e85f9 Merge pull request #31 from dexoron/dev
v0.8.4
2026-08-05 21:22:20 +08:00
a20c61cc88 docs: add and clean up AI-generated rustdoc comments (grok-4.5) 2026-08-05 19:21:00 +08:00
bc308d6996 fix: make cross-target tests and vscode launch path robust for macOS + Windows 2026-08-05 02:21:29 +08:00
09e7f5b880 fix(test): align vscode launch path assertion with native host layout 2026-08-05 01:04:43 +08:00
3f925f437f fix: keep native host artifact paths and cross-platform path assertions 2026-08-05 01:00:26 +08:00
b9cbef43e9 feat: add .dcr/ build facts and canonical artifact paths for IDE integrations 2026-08-05 00:38:00 +08:00
9bf1346d1b Merge pull request #30 from dexoron/dev
v0.8.3
2026-08-03 21:32:18 +08:00
794d571d58 fix: distinguish implicit host builds from explicit targets 2026-08-03 21:26:34 +08:00
d2df85a064 fix: preserve native artifact paths on Windows and macOS 2026-08-03 21:16:11 +08:00
5e8037da97 feat: host-target config resolution and cargo-style run args 2026-08-03 00:40:51 +08:00
f29322a547 Merge pull request #29 from dexoron/dev
v0.8.2
2026-07-24 19:15:55 +08:00
1a6a261cac fix(ci): fix tests 2026-07-23 21:35:59 +08:00
f92cc6ae91 fix(ci): fix tests 2026-07-23 19:59:37 +08:00
5aefbc6498 fix(ci): fix tests 2026-07-23 19:53:48 +08:00
594007f022 fix(ci): fix tests 2026-07-23 19:45:31 +08:00
d10ec84004 fix(ci): fix tests 2026-07-23 19:30:39 +08:00
574ba8b6d0 fix(ci): fix tests 2026-07-23 16:11:39 +08:00
75eace9d73 feat: add dcrup (sh/ps1/cmd) and fix cross-platform unit tests 2026-07-23 14:59:28 +08:00
2446cb7970 feat(docs): remove DPM in install steps 2026-07-23 14:03:08 +08:00
689fea7465 Merge pull request #28 from dexoron/dev
v0.8.1
2026-07-21 18:12:56 +08:00
1dcc8e614a Merge pull request #25 from dexoron/dev
v0.8.0
2026-07-18 20:05:12 +08:00
a3f8444c20 Replace GitLab pipeline trigger with Cloudflare build 2026-07-05 18:24:47 +08:00
1f3fc62cb6 Add GitHub Actions workflow to trigger site rebuild 2026-07-05 18:16:45 +08:00
f8d8c95912 Rename сrowdin.yml to crowdin.yml (latin) 2026-07-05 17:26:00 +08:00
9e07f70c78 fix crowdin file name 2026-07-05 17:24:14 +08:00
0376eded3f add Crowdin 2026-07-05 17:21:33 +08:00
803d5891ce Merge pull request #21 from dexoron/dev
v0.7.4
2026-06-19 17:50:59 +08:00
97db82e4f8 Merge pull request #20 from dexoron/dev
v0.7.3
2026-06-15 22:36:12 +08:00
112 changed files with 8417 additions and 2325 deletions

View File

@@ -6,11 +6,25 @@ 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
RUSTFLAGS: -Dwarnings
CARGO_INCREMENTAL: "0"
SCCACHE_GHA_ENABLED: "true"
jobs:
checks:
@@ -24,7 +38,13 @@ jobs:
with:
components: rustfmt, clippy
- name: sccache
uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
shared-key: dcr-ci
cache-all-crates: true
- name: Format
run: cargo fmt --all --check
@@ -52,7 +72,22 @@ jobs:
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: sccache
uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
shared-key: dcr-ci
cache-all-crates: true
cache-on-failure: true
- name: Cache apt packages
if: runner.os == 'Linux'
uses: actions/cache@v4
with:
path: /var/cache/apt/archives
key: apt-${{ runner.os }}-${{ hashFiles('.github/workflows/ci.yml') }}
restore-keys: apt-${{ runner.os }}-
- name: Install Linux tools
if: runner.os == 'Linux'
@@ -64,7 +99,8 @@ jobs:
nasm \
clang-format \
clang-tidy \
git
git \
llvm
- name: Install macOS tools
if: runner.os == 'macOS'
@@ -76,8 +112,11 @@ jobs:
if: runner.os == 'Windows'
shell: pwsh
run: |
choco install nasm -y
choco install nasm llvm -y
echo "C:\Program Files\NASM" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
if (Test-Path "C:\Program Files\LLVM\bin") {
echo "C:\Program Files\LLVM\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
}
- name: Unit tests (binary crate)
run: cargo test --bins -- --nocapture
@@ -89,6 +128,10 @@ jobs:
if: runner.os != 'Windows'
run: cargo test --tests --features archive -- --nocapture
- name: sccache stats
if: always()
run: sccache --show-stats || true
test-archive-linux:
name: archive feature smoke
needs: checks
@@ -99,15 +142,25 @@ jobs:
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: sccache
uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
shared-key: dcr-ci
cache-all-crates: true
- name: Install tools
run: |
sudo apt-get update
sudo apt-get install -y build-essential clang nasm
sudo apt-get install -y build-essential clang nasm llvm
- name: Build with archive
run: cargo build --features archive
- name: Flat-bin + unit suite with archive
run: cargo test --features archive --test cli_build --test cli_workspace --test cli_errors -- --nocapture
- name: sccache stats
if: always()
run: sccache --show-stats || true

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

View File

@@ -0,0 +1,14 @@
name: Trigger site rebuild
on:
push:
branches: [master]
paths:
- 'docs/**'
jobs:
trigger:
runs-on: ubuntu-latest
steps:
- name: Trigger Cloudflare Pages build
run: curl -sS -X POST "${{ secrets.CF_PAGES_DEPLOY_HOOK }}"

2
.gitignore vendored
View File

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

View File

@@ -1,5 +1,231 @@
# Changelog
## [v0.9.5] - 2026-10-21 "Структурированный вывод CLI / Structured CLI Output"
### RU
**Добавлено:**
- Человекочитаемый, plain- и JSON-режимы вывода в стиле Cargo с общими хелперами вывода.
- Явная обработка `--color auto|always|never`, `NO_COLOR`, `--quiet` и `--verbose`.
- Структурированные события жизненного цикла сборки и JSON-диагностика компилятора.
- Сценарии CLI output sandbox для help, жизненного цикла, диагностики, ошибок и broken pipe.
**Исправлено:**
- Единообразная маршрутизация stdout/stderr и обработка цветов во всех основных командах.
- Глобальный парсер опций теперь останавливается на subcommand и сохраняет аргументы после `--`.
- Перенаправленный вывод подпроцессов теперь захватывается конкурентно в output lab.
### EN
**Added:**
- Cargo-like human, plain, and JSON output modes with shared output helpers.
- Explicit `--color auto|always|never`, `NO_COLOR`, `--quiet`, and `--verbose` handling.
- Structured build lifecycle events and JSON compiler diagnostics.
- CLI output sandbox scenarios for help, lifecycle, diagnostics, errors, and broken pipes.
**Fixed:**
- Consistent stdout/stderr routing and color handling across the main commands.
- Global option parsing now stops at the subcommand and preserves arguments after `--`.
- Redirected subprocess output is captured concurrently in the output lab.
## [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
**Добавлено:**
- **`package.dcr-version`** — опциональный pin версии CLI в `dcr.toml`.
`dcr new` / `dcr init` пишут текущую версию. При загрузке конфига:
- нет / пустой pin → warn «добавь dcr-version»;
- dcr старше pin → warn «обнови dcr»;
- dcr новее pin → warn «проверь changelog, подними pin»;
- равенство — тишина.
Команды не падают, только предупреждение (раз на path за процесс).
### EN
**Added:**
- **`package.dcr-version`** — optional CLI version pin in `dcr.toml`.
`dcr new` / `dcr init` write the current version. On config load:
- missing / empty pin → warn "add dcr-version";
- dcr older than pin → warn "update dcr";
- dcr newer than pin → warn "check changelog, update pin";
- equal → silent.
Commands do not fail, warning only (once per path per process).
## [0.8.4] - 2026-08-04 "Инфраструктура .dcr/ и интеграция с IDE / Internal .dcr/ Layout & IDE Integration"
### RU
**Добавлено:**
- **Директория `.dcr/`** — единый machine-readable источник фактов о сборке для IDE/расширений:
- `.dcr/compile_commands.json`
- `.dcr/build-info.json` (`schemaVersion`, `artifact_path`, `profile`, …)
- `.dcr/toolchain.json` (compiler/debugger paths, moc/uic/rcc)
- `.dcr/ide/` (заготовки lock-файлов для расширений)
- **`dcr gen project-info`** — поля `target`, `target_dir`, `artifact_path`, `artifact_kind`, `source_roots`, `include_globs`, `exclude_globs`; пути абсолютные/канонические.
- **`dcr build --print-artifact-path`** — после успешной сборки одна строка absolute path на stdout.
- **`dcr gen … --quiet` / `-q`** — без human-лога на stdout для `compile-commands` / `project-info`.
- **`.clangd`** — при первом `gen` создаётся с `CompilationDatabase: .dcr` (маркер `# managed by dcr gen`); ручной `.clangd` не перезаписывается.
- **Хелперы** `canonicalize_path`, `atomic_write`, `ensure_dcr_dir`; pure `resolve_artifact_path` через `platform::*`.
- **`dcr new` / `dcr init`** — `.dcr/` в `.gitignore`.
**Изменено (breaking):**
- **`compile_commands.json` перенесён** из корня проекта в **`.dcr/compile_commands.json`**.
clangd находит базу через `.clangd` / `clangd.arguments --compile-commands-dir=.dcr`.
CLion `misc.xml` и VSCode `settings.json` обновлены под новый путь.
- **Атомарная запись** `compile_commands.json` (tmp + rename).
- **`gen vscode` / `gen clion` launch/run** — `program`/`executable` через тот же artifact resolver, что `project-info` / `build-info.json` (Linux triple, Windows `.exe`, custom `target`/`filename`).
**Исправлено:**
- Launch-конфиги больше не указывают `target/<profile>/<name>` без triple на Linux.
- Симлинки/mount: root и пути в compile database канонизируются best-effort.
### EN
**Added:**
- **`.dcr/` directory** — shared machine-readable build facts for IDE integrations:
- `.dcr/compile_commands.json`
- `.dcr/build-info.json` (`schemaVersion`, `artifact_path`, `profile`, …)
- `.dcr/toolchain.json` (compiler/debugger paths, moc/uic/rcc)
- `.dcr/ide/` (placeholder for IDE lock files)
- **`dcr gen project-info`** — `target`, `target_dir`, `artifact_path`, `artifact_kind`, `source_roots`, `include_globs`, `exclude_globs`; absolute/canonical paths.
- **`dcr build --print-artifact-path`** — one absolute artifact path line on stdout after a successful build.
- **`dcr gen … --quiet` / `-q`** — suppress human success logs for gen subcommands.
- **`.clangd`** — created on first gen with `CompilationDatabase: .dcr` (`# managed by dcr gen`); custom `.clangd` is not overwritten.
- **Helpers** `canonicalize_path`, `atomic_write`, `ensure_dcr_dir`; pure `resolve_artifact_path` via `platform::*`.
- **`dcr new` / `dcr init`** — add `.dcr/` to `.gitignore`.
**Changed (breaking):**
- **`compile_commands.json` moved** from project root to **`.dcr/compile_commands.json`**.
clangd discovers it via `.clangd` / `--compile-commands-dir=.dcr`.
CLion and VSCode generators updated accordingly.
- **Atomic write** for compile_commands (tmp + rename).
- **`gen vscode` / `gen clion` launch/run** — use the same artifact path resolver as `project-info` / `build-info.json`.
**Fixed:**
- Launch configs no longer hardcode `target/<profile>/<name>` without the Linux triple segment.
- Symlink/mount roots: best-effort canonicalize for root and compile database paths.
## [0.8.3] - 2026-08-03 "Конфигурация host target, аргументы run и нативные пути артефактов / Host Target Config, Run Args & Native Artifact Paths"
### RU
**Добавлено:**
- **`dcr run -- <args>...`** — cargo-style: всё после bare `--` передаётся собранному бинарнику
(`dcr run --release -- --test_help`). Работает и для прямого запуска, и для `[run].cmd`.
- **Интеграционные / unit-тесты** — host target sections применяют `ldflags` без `--target`; проброс аргументов после `--`; парсер `split_double_dash`.
**Изменено:**
- **Native-сборка резолвит host triple** — без `--target` / без `build.target` конфиг-секции вида
`[build.x86_64-unknown-linux-gnu]` (и аналоги host) снова применяются (`cflags` / `ldflags` / toolchain).
Раньше target был «пустым», секции молча игнорировались, а артефакты уже лежали под host triple — линковка без `-L`/`-l`.
- **Пустой target** — `normalize_target_os("")` больше не warn'ит `Unknown target ''`.
- **`dcr run --help` / man `dcr-run`** — документирован разделитель `--` и forwarding args.
**Исправлено:**
- **Нативные пути артефактов на Windows и macOS** — без явного `--target` / `build.target` артефакты снова сохраняются в `target/<profile>`, поэтому `dcr run` и flat-bin находят результат сборки.
- Target-specific `ldflags` на native Linux/macOS/Windows (host triple) не доходили до линкера.
- Ложный warning на пустой target string.
### EN
**Added:**
- **`dcr run -- <args>...`** — cargo-style: arguments after a bare `--` are forwarded to the built binary
(`dcr run --release -- --test_help`). Works for direct binary runs and `[run].cmd`.
- **Tests** — host target sections apply `ldflags` without CLI `--target`; run-arg forwarding; `split_double_dash` unit tests.
**Changed:**
- **Native builds resolve the host triple** — without `--target` / without package `build.target`, host sections such as
`[build.x86_64-unknown-linux-gnu]` (and host equivalents) apply again (`cflags` / `ldflags` / toolchain).
Previously the target was empty, those sections were silently ignored while artifacts already used the host triple path — link failed without `-L`/`-l` (e.g. sandbox micro-lang + `libsct-elf`).
- **Empty target** — `normalize_target_os("")` no longer warns `Unknown target ''`.
- **`dcr run --help` / man `dcr-run`** — document `--` and argument forwarding.
**Fixed:**
- **Native artifact paths on Windows and macOS** — without an explicit `--target` / `build.target`, artifacts again use `target/<profile>`, so `dcr run` and flat-bin find the build output.
- Target-specific `ldflags` on native host builds not reaching the linker.
- Spurious warning for an empty target string.
## [0.8.2] - 2026-07-22 "CLI Progress, archive Feature, Workspace Path Fixes / CLI Progress, archive Feature, Workspace Path Fixes"
### RU
@@ -215,25 +441,31 @@
### RU
**Добавлено:**
- Учтён `target_env` в `default_target_triple()` — Linux (gnu/musl) и Windows (msvc/gnu) теперь используют правильное окружение, а не хардкод.
**Исправлено:**
- **Сборка на macOS Apple Silicon падала с `_main` undefined** — `--target=` теперь передаётся и в ldflags, не только в cflags. `default_target_triple()` на macOS использует `std::env::consts::ARCH` вместо хардкода `x86_64`.
- **Бейджи GitHub Stars и GPL-3.0 не рендерились в README** — блок бейджей переведён на чистый HTML.
**Изменено:**
- Вычисление таргета по умолчанию вынесено в общую функцию `default_target_triple()` — устранено дублирование в build, run, clean.
### EN
**Added:**
- `default_target_triple()` now respects `target_env` — Linux (gnu/musl) and Windows (msvc/gnu) use the correct environment instead of hardcoded values.
**Fixed:**
- **macOS Apple Silicon builds failed with `_main` undefined** — `--target=` is now passed to ldflags, not just cflags. `default_target_triple()` uses `std::env::consts::ARCH` on macOS instead of hardcoded `x86_64`.
- **GitHub Stars and GPL-3.0 badges not rendering in README** — badge block converted to pure HTML.
**Changed:**
- Default target resolution extracted into shared `default_target_triple()` — removed duplication across build, run, clean.
## [0.7.1] - 2026-06-02 "Мультиархитектурное расширение / The Multi-Arch Expansion"
@@ -241,6 +473,7 @@
### RU
**Добавлено:**
- Вывод `--help` для всех команд с цветным форматированием.
- man-страницы (12 штук в `man/man1/`) и их установка во всех пакетах.
- Валидация имен проектов в `dcr init` и `dcr new` (только ASCII буквы, цифры, `_` и `-`).
@@ -251,6 +484,7 @@
- Пакеты AUR (`dcr-dev`, `dcr-dev-bin`) и сборка Snap в CI.
**Изменено:**
- Функция `validate_package_name` стала публичной.
- Команды `gen`, `tree`, `fmt`, `setup` больше не игнорируют аргументы и используют цветной вывод.
- Инструкция в README изменена с `| bash` на `| sh` для POSIX.
@@ -268,6 +502,7 @@
### EN
**Added:**
- `--help` for all commands with styled headers and usage lines.
- Man pages (12 troff man pages in `man/man1/`) and their installation across all packaging formats.
- Project name validation in `dcr init` and `dcr new` (ASCII letters, digits, `_`, `-` only).
@@ -278,6 +513,7 @@
- AUR packages (`dcr-dev`, `dcr-dev-bin`) and Snap build in CI.
**Changed:**
- `validate_package_name` made public and callable from CLI commands directly.
- `gen.rs`, `tree.rs`, `fmt.rs`, `setup.rs` no longer ignore arguments and use styled output.
- README install instructions changed from `| bash` to `| sh` for POSIX compatibility.
@@ -454,6 +690,7 @@ Added:
```
Produces `KERNEL.EFI` (works for `bin`, `staticlib`, and `sharedlib`).
- Automatic injection of `--target=<build.target>` into compiler flags when `build.target` is set in `dcr.toml`. This greatly simplifies clang-based cross-compilation (especially for bare-metal targets like `aarch64-none-elf`).
- Bare-metal targets (containing `none`, `-elf`, `eabi`, `baremetal`) no longer receive DCR's internal default flags (`-g`, `-Wall`, `-Wextra`, `-fno-omit-frame-pointer`, `-DDCR_DEBUG`, etc.). Prevents unwanted sections (`.comment`, debug info, etc.) that break custom linker scripts when `inherit = true`.
- `build.ldscript` in `dcr.toml` — linker script path passed as `-T <path>` to the linker. Essential for bare-metal/embedded/freestanding targets.
@@ -488,28 +725,28 @@ Fixed:
- Path dependencies now work correctly: `dcr add` stores them as `{ path =
"./..." }` tables, `is_registry_dep` properly distinguishes registry strings
from path/git strings, and `deps/mod.rs` resolves both table-form and
legacy string-form path dependencies
from path/git strings, and `deps/mod.rs` resolves both table-form and
legacy string-form path dependencies
- Registry dependency paths no longer hardcoded to `project_root/dcr-index`;
now use `package_root_from_registry_info()` which resolves relative to the
registry cache root (`~/.dcr/`)
now use `package_root_from_registry_info()` which resolves relative to the
registry cache root (`~/.dcr/`)
- Registry dependencies are now actually built: `build_project_at()` is
called when `include_dir` or `lib_dir` is missing
called when `include_dir` or `lib_dir` is missing
Added:
- `SIGINT`/`Ctrl+C` handler via `ctrlc` crate — `dcr build` now checks
`BUILD_INTERRUPTED` flag at key points and aborts cleanly
`BUILD_INTERRUPTED` flag at key points and aborts cleanly
- `utils/build.rs` — extracted shared utilities: `parse_version_info`,
`normalize_target_os`, `resolve_compiler`, `primary_language`,
`resolve_pkg_config_flags` and config helpers. Eliminates code duplication
between `cli/build.rs`, `cli/run.rs`, `cli/clean.rs`, `cli/gen.rs`
`normalize_target_os`, `resolve_compiler`, `primary_language`,
`resolve_pkg_config_flags` and config helpers. Eliminates code duplication
between `cli/build.rs`, `cli/run.rs`, `cli/clean.rs`, `cli/gen.rs`
- `utils/fs.rs::with_dir` — extracted common directory-scoped execution
- `run_command_sync_output` in `builder/common.rs` with global `OUTPUT_MUTEX`
— synchronized compiler output across all backends (unix_cc, gas, nasm, msvc)
— synchronized compiler output across all backends (unix_cc, gas, nasm, msvc)
- New helper functions in `deps/register.rs`: `get_registry_cache_root`,
`package_root_from_registry_info`, `registry_include_dir`, `registry_lib_dir`,
`path_from_string_dep`
`package_root_from_registry_info`, `registry_include_dir`, `registry_lib_dir`,
`path_from_string_dep`
- Unit tests for `register.rs` (`is_registry_dep`, `package_root_from_registry_info`),
`deps/mod.rs` (`path_dep_path`, `push_default_lib_dirs`),
`utils/build.rs` (`normalize_target_os`, `parse_version_info`)

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
```

333
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.2"
version = "0.9.5"
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]]
@@ -330,6 +452,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
[[package]]
name = "iana-time-zone"
version = "0.1.65"
@@ -475,12 +603,71 @@ dependencies = [
"serde_core",
]
[[package]]
name = "is-terminal"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "is_ci"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
[[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 +705,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 +757,28 @@ 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"
dependencies = [
"supports-color 2.1.0",
"supports-color 3.0.2",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -588,6 +791,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 +822,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -631,6 +849,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 +1004,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -817,6 +1064,25 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "supports-color"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89"
dependencies = [
"is-terminal",
"is_ci",
]
[[package]]
name = "supports-color"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6"
dependencies = [
"is_ci",
]
[[package]]
name = "syn"
version = "2.0.117"
@@ -828,6 +1094,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 +1113,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -852,6 +1129,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 +1273,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 +1335,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
"wasm-bindgen-shared",
]
@@ -1100,7 +1403,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -1111,7 +1414,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -1271,7 +1574,7 @@ dependencies = [
"heck",
"indexmap",
"prettyplease",
"syn",
"syn 2.0.117",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
@@ -1287,7 +1590,7 @@ dependencies = [
"prettyplease",
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
"wit-bindgen-core",
"wit-bindgen-rust",
]
@@ -1354,7 +1657,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
"synstructure",
]
@@ -1375,7 +1678,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
"synstructure",
]
@@ -1415,7 +1718,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.2"
version = "0.9.5"
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 = { version = "4", features = ["supports-colors"] }
[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

@@ -34,38 +34,15 @@
<table>
<tr>
<td><b>Linux (Dexoron Packages)</b></td>
<td>
See https://pkg.dexoron.su for settings youre packages manager
**Arch Linux:**
```sh
sudo pacman -Sy dcr
```
**Debian/Ubuntu:**
```sh
sudo apt update && sudo apt install dcr
```
**Fedora/RHEL:**
```sh
sudo dnf install dcr
```
</td>
</tr>
<tr>
<td><b>Arch Linux (AUR)</b></td>
<td>
```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>
@@ -103,21 +80,32 @@ cargo install dcr
</td>
</tr>
<tr>
<td><b>Linux / macOS (script)</b></td>
<td><b>dcrup (Linux / macOS / BSD / Windows(Bash))</b></td>
<td>
```sh
curl -fsSL https://dcr.dexoron.su/install.sh | sh
curl -fsSL https://sh.dcr-tool.ru | sh -s -- self-install
export PATH="$HOME/.dcr/bin:$PATH"
dcrup install stable # auto: cf → gh → sf → ya
# dcrup install stable --mirror cf # CloudFlare CDN
# dcrup install stable --mirror ya # Yandex CDN (RU)
```
</td>
</tr>
<tr>
<td><b>BSD (script)</b></td>
<td><b>dcrup (Windows PowerShell)</b></td>
<td>
```sh
curl -fsSL https://dcr.dexoron.su/install_bsd.sh | sh
```powershell
irm https://ps1.dcr-tool.ru | iex
# or:
# irm https://ps1.dcr-tool.ru -OutFile dcrup.ps1
# powershell -File .\dcrup.ps1 self-install
# $env:Path += ";$env:USERPROFILE\.dcr\bin"
dcrup install stable # auto: cf → gh → sf → ya
# dcrup install stable --mirror cf # CloudFlare CDN
# dcrup install stable --mirror ya # Yandex CDN (RU)
```
</td>
@@ -133,16 +121,6 @@ curl -fsSL https://dcr.dexoron.su/install_bsd.sh | sh
sudo snap install --classic --dangerous dcrup_*.snap
```
</td>
</tr>
<tr>
<td><b>Windows (PowerShell)</b></td>
<td>
```powershell
irm https://dcr.dexoron.su/install.ps1 | iex
```
</td>
</tr>
<tr>
@@ -151,7 +129,7 @@ irm https://dcr.dexoron.su/install.ps1 | iex
```sh
git clone https://github.com/dexoron/dcr.git
cd dcr && cargo build --release
cd dcr && cargo build --release --features archive
ln -sf "$PWD/target/release/dcr" ~/.local/bin/dcr
```
@@ -245,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">

3
crowdin.yml Normal file
View File

@@ -0,0 +1,3 @@
files:
- source: /docs/**/*.mdx
translation: /docs-crowdin-export/%locale%/%original_path%/%original_file_name%.%file_extension%

470
dcrup.ps1 Normal file
View File

@@ -0,0 +1,470 @@
#Requires -Version 5.1
$ErrorActionPreference = 'Stop'
$DcrupHome = if ($env:DCRUP_HOME) { $env:DCRUP_HOME } else { Join-Path $env:USERPROFILE '.dcr' }
$DcrupBin = if ($env:DCRUP_BIN) { $env:DCRUP_BIN } else { Join-Path $DcrupHome 'bin' }
$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' }
function Info($m) { Write-Host "[dcrup] $m" -ForegroundColor Cyan }
function Ok($m) { Write-Host "[ok] $m" -ForegroundColor Green }
function Warn($m) { Write-Host "[warn] $m" -ForegroundColor Yellow }
function Die($m) { Write-Host "[error] $m" -ForegroundColor Red; exit 1 }
function Show-Usage {
@'
dcrup — install and switch DCR versions (Windows)
Usage:
dcrup install <spec> [--build|--release] [--force] [--mirror auto|cf|gh|sf|ya]
dcrup default <spec>
dcrup update
dcrup list
dcrup show
dcrup which
dcrup uninstall [<spec>|--all]
dcrup self-install [--to DIR]
dcrup help
Spec:
stable | dev | night
VERSION → VERSION@stable (e.g. 0.8.2)
VERSION@stable
VERSION@dev
night → always build from branch "dev" HEAD
Modes:
--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
}
function Ensure-Dirs {
New-Item -ItemType Directory -Force -Path $DcrupTc, $DcrupBin, $DcrupMeta | Out-Null
}
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 { Die "unsupported arch: $arch" }
}
}
function Normalize-Version([string]$v) {
if ($v.StartsWith('v')) { return $v.Substring(1) }
return $v
}
function Parse-Spec([string]$raw) {
$script:SpecRaw = $raw
$script:SpecChannel = $null
$script:SpecVersion = $null
$script:SpecFloating = $false
if ($raw -in @('stable', 'dev', 'night')) {
$script:SpecChannel = $raw
$script:SpecFloating = $true
return
}
if ($raw -match '^(.*)@(.*)$') {
$script:SpecVersion = Normalize-Version $Matches[1]
$script:SpecChannel = $Matches[2]
if ($script:SpecChannel -eq 'night') { Die 'cannot pin version on night' }
if ($script:SpecChannel -notin @('stable', 'dev')) { Die "unknown channel: $($script:SpecChannel)" }
return
}
$script:SpecVersion = Normalize-Version $raw
$script:SpecChannel = 'stable'
}
function Toolchain-Id([string]$extra = '') {
if ($script:SpecChannel -eq 'night') {
return "night-$extra"
}
if ($script:SpecFloating) {
return "$($script:SpecChannel)-$extra"
}
return "$($script:SpecVersion)-$($script:SpecChannel)"
}
function Active-Path { Join-Path $DcrupMeta 'active' }
function Read-Active {
$p = Active-Path
if (Test-Path $p) { return (Get-Content $p -Raw).Trim() }
return $null
}
function Write-Active([string]$id) {
Set-Content -Path (Active-Path) -Value $id -NoNewline
}
function Link-Default([string]$id) {
$src = Join-Path (Join-Path $DcrupTc $id) 'dcr.exe'
if (-not (Test-Path $src)) { Die "toolchain not installed: $id" }
$dst = Join-Path $DcrupBin 'dcr.exe'
Copy-Item $src $dst -Force
Write-Active $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"
}
if ($channel -eq 'dev') {
$all = Invoke-RestMethod -Uri $ApiAll
$pre = $all | Where-Object { $_.prerelease } | Select-Object -First 1
if (-not $pre) { Die 'no prerelease (dev) found' }
return $pre
}
return Invoke-RestMethod -Uri $ApiLatest
}
function Install-Prebuilt([switch]$Force) {
$triple = Get-Target
$rel = Fetch-Release $script:SpecChannel $script:SpecVersion
$tag = $rel.tag_name
$ver = Normalize-Version $tag
$assetName = "dcr-$triple-$ver.exe"
$id = if ($script:SpecFloating) { Toolchain-Id $ver } else { Toolchain-Id }
$dest = Join-Path $DcrupTc $id
$bin = Join-Path $dest 'dcr.exe'
if ((Test-Path $bin) -and -not $Force) {
Ok "already installed: $id"
Link-Default $id
return
}
Ensure-Dirs
New-Item -ItemType Directory -Force -Path $dest | Out-Null
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 ($triple via $mirror)"
Link-Default $id
}
function Install-Build([switch]$Force) {
if (-not (Get-Command git -ErrorAction SilentlyContinue)) { Die 'git not found' }
if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { Die 'cargo not found' }
$tmp = Join-Path $env:TEMP ("dcrup-" + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $tmp | Out-Null
try {
$src = Join-Path $tmp 'src'
if ($script:SpecChannel -eq 'night') {
Info "cloning dev for night …"
git clone --depth 1 --branch dev $RepoUrl $src 2>$null
if ($LASTEXITCODE -ne 0) { git clone --depth 1 $RepoUrl $src | Out-Null }
$sha = (git -C $src rev-parse --short HEAD).Trim()
$id = Toolchain-Id $sha
$ver = 'night'
} elseif ($script:SpecFloating) {
$rel = Fetch-Release $script:SpecChannel $null
$tag = $rel.tag_name
$ver = Normalize-Version $tag
$id = Toolchain-Id $ver
Info "cloning $tag …"
git clone --depth 1 --branch $tag $RepoUrl $src 2>$null
if ($LASTEXITCODE -ne 0) {
git clone --depth 1 $RepoUrl $src | Out-Null
git -C $src fetch --depth 1 origin "refs/tags/${tag}:refs/tags/${tag}"
git -C $src checkout $tag
}
$sha = (git -C $src rev-parse --short HEAD).Trim()
} else {
$tag = "v$($script:SpecVersion)"
$ver = $script:SpecVersion
$id = Toolchain-Id
Info "cloning $tag …"
git clone --depth 1 --branch $tag $RepoUrl $src 2>$null
if ($LASTEXITCODE -ne 0) {
git clone --depth 1 $RepoUrl $src | Out-Null
git -C $src fetch --depth 1 origin "refs/tags/${tag}:refs/tags/${tag}"
git -C $src checkout $tag
}
$sha = (git -C $src rev-parse --short HEAD).Trim()
}
$dest = Join-Path $DcrupTc $id
$bin = Join-Path $dest 'dcr.exe'
if ((Test-Path $bin) -and -not $Force) {
Ok "already installed: $id"
Link-Default $id
return
}
$triple = Get-Target
Info "building release (features: $Features) …"
Push-Location $src
cargo build --release --features $Features --target $triple
if ($LASTEXITCODE -ne 0) { Die 'cargo build failed' }
Pop-Location
New-Item -ItemType Directory -Force -Path $dest | Out-Null
Copy-Item (Join-Path $src "target\$triple\release\dcr.exe") $bin -Force
@"
channel=$($script:SpecChannel)
version=$ver
git=$sha
source=build
features=$Features
"@ | Set-Content (Join-Path $dest 'dcrup-meta')
Ok "installed build $id"
Link-Default $id
} finally {
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
function Ensure-UserPath {
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
if (-not $userPath) { $userPath = '' }
if (-not ($userPath -split ';' | Where-Object { $_ -eq $DcrupBin })) {
$new = if ($userPath) { "$userPath;$DcrupBin" } else { $DcrupBin }
[Environment]::SetEnvironmentVariable('Path', $new, 'User')
$env:Path = "$env:Path;$DcrupBin"
Warn "added to user PATH: $DcrupBin (restart shell if needed)"
}
}
function Install-CmdShim {
$cmd = Join-Path $DcrupBin 'dcrup.cmd'
$ps1 = Join-Path $DcrupBin 'dcrup.ps1'
@"
@echo off
setlocal
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0dcrup.ps1" %*
"@ | Set-Content -Path $cmd -Encoding ASCII
if (-not (Test-Path $ps1)) {
Copy-Item $PSCommandPath $ps1 -Force
}
}
function Cmd-Install {
param([string[]]$Args)
$mode = 'release'
$force = $false
$spec = $null
$i = 0
while ($i -lt $Args.Count) {
$a = $Args[$i]
switch -Regex ($a) {
'^--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] [--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 mirror=$($script:DcrupMirror)"
if ($mode -eq 'build') { Install-Build -Force:$force } else { Install-Prebuilt -Force:$force }
Ensure-UserPath
Install-CmdShim
}
function Cmd-List {
Ensure-Dirs
$active = Read-Active
Get-ChildItem $DcrupTc -Directory -ErrorAction SilentlyContinue | ForEach-Object {
$mark = if ($_.Name -eq $active) { ' *' } else { ' ' }
Write-Host "$mark $($_.Name)"
}
}
function Cmd-Show {
Ensure-Dirs
Write-Host "DCRUP_HOME=$DcrupHome"
Write-Host "DCRUP_BIN=$DcrupBin"
Write-Host "active=$(Read-Active)"
$exe = Join-Path $DcrupBin 'dcr.exe'
if (Test-Path $exe) { & $exe --version }
}
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 …)"
}
$cmd = if ($args.Count -ge 1) { $args[0] } else { 'help' }
$rest = @()
if ($args.Count -gt 1) { $rest = $args[1..($args.Count - 1)] }
switch ($cmd) {
{ $_ -in @('help', '-h', '--help') } { Show-Usage }
'install' { Cmd-Install $rest }
'list' { Cmd-List }
'show' { Cmd-Show }
'which' {
$p = Join-Path $DcrupBin 'dcr.exe'
if (Test-Path $p) { $p } else { Die 'dcr not installed' }
}
'default' {
if ($rest.Count -ne 1) { Die 'usage: dcrup default <spec>' }
Parse-Spec $rest[0]
if ($script:SpecFloating -or $script:SpecChannel -eq 'night') {
$id = Get-ChildItem $DcrupTc -Directory |
Where-Object { $_.Name -like "$($script:SpecChannel)-*" } |
Sort-Object Name | Select-Object -Last 1
if (-not $id) { Die "not installed: $($rest[0])" }
Link-Default $id.Name
} else {
Link-Default (Toolchain-Id)
}
}
'update' {
$a = Read-Active
if (-not $a) { Die 'no active toolchain' }
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" }
}
'uninstall' {
if ($rest -contains '--all') {
Remove-Item -Recurse -Force $DcrupTc -ErrorAction SilentlyContinue
Remove-Item (Join-Path $DcrupBin 'dcr.exe') -ErrorAction SilentlyContinue
Remove-Item (Active-Path) -ErrorAction SilentlyContinue
Ok 'removed all'
} else {
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" }
}

700
dcrup.sh Normal file
View File

@@ -0,0 +1,700 @@
#!/usr/bin/env bash
set -Eeuo pipefail
DCRUP_HOME="${DCRUP_HOME:-$HOME/.dcr}"
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}"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
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
}
usage() {
cat <<'EOF'
dcrup — install and switch DCR versions
Usage:
dcrup install <spec> [--build|--release] [--force] [--libc gnu|musl|auto] [--mirror auto|cf|gh|sf|ya]
dcrup default <spec>
dcrup update
dcrup list
dcrup show
dcrup which
dcrup uninstall [<spec>|--all]
dcrup self-install [--to DIR]
dcrup help
Spec:
stable | dev | night
VERSION → VERSION@stable (e.g. 0.8.2)
VERSION@stable
VERSION@dev
night → always build from branch "dev" HEAD
Modes:
--release download prebuilt binary (default for stable/dev)
--build cargo build --release --features archive
night always --build (prebuilt not available)
Linux libc (prebuilt triple only):
--libc gnu x86_64-unknown-linux-gnu (default)
--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
}
need_cmd() {
command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
}
detect_linux_libc() {
local pref="${DCRUP_LIBC:-gnu}"
case "$pref" in
gnu | glibc) echo "gnu" ;;
musl) echo "musl" ;;
auto)
if command -v ldd >/dev/null 2>&1; then
if ldd --version 2>&1 | grep -qi musl; then
echo "musl"
return 0
fi
fi
if [[ -e /lib/ld-musl-x86_64.so.1 || -e /lib/ld-musl-aarch64.so.1 ]]; then
echo "musl"
return 0
fi
if command -v getconf >/dev/null 2>&1 && getconf GNU_LIBC_VERSION >/dev/null 2>&1; then
echo "gnu"
return 0
fi
echo "gnu"
;;
*) die "invalid DCRUP_LIBC / --libc: $pref (use gnu|musl|auto)" ;;
esac
}
detect_triple() {
local os arch libc
os="$(uname -s)"
arch="$(uname -m)"
case "$os:$arch" in
Linux:x86_64)
libc="$(detect_linux_libc)"
if [[ "$libc" == "musl" ]]; then
echo "x86_64-unknown-linux-musl"
else
echo "x86_64-unknown-linux-gnu"
fi
;;
Linux:aarch64 | Linux:arm64)
libc="$(detect_linux_libc)"
if [[ "$libc" == "musl" ]]; then
echo "aarch64-unknown-linux-musl"
else
echo "aarch64-unknown-linux-gnu"
fi
;;
Linux:i686 | Linux:i386)
libc="$(detect_linux_libc)"
if [[ "$libc" == "musl" ]]; then
echo "i686-unknown-linux-musl"
else
echo "i686-unknown-linux-gnu"
fi
;;
Linux:armv7l | Linux:armv7)
libc="$(detect_linux_libc)"
if [[ "$libc" == "musl" ]]; then
echo "armv7-unknown-linux-musleabihf"
else
echo "armv7-unknown-linux-gnueabihf"
fi
;;
Linux:riscv64)
libc="$(detect_linux_libc)"
if [[ "$libc" == "musl" ]]; then
echo "riscv64gc-unknown-linux-musl"
else
echo "riscv64gc-unknown-linux-gnu"
fi
;;
Darwin:x86_64) echo "x86_64-apple-darwin" ;;
Darwin:arm64 | Darwin:aarch64) echo "aarch64-apple-darwin" ;;
FreeBSD:x86_64 | FreeBSD:amd64) echo "x86_64-unknown-freebsd" ;;
FreeBSD:aarch64 | FreeBSD:arm64) echo "aarch64-unknown-freebsd" ;;
OpenBSD:x86_64 | OpenBSD:amd64) echo "x86_64-unknown-openbsd" ;;
NetBSD:x86_64 | NetBSD:amd64) echo "x86_64-unknown-netbsd" ;;
MINGW* | MSYS* | CYGWIN*)
case "$arch" in
x86_64 | amd64) echo "x86_64-pc-windows-gnu" ;;
aarch64 | arm64) echo "aarch64-pc-windows-gnullvm" ;;
i686 | i386) echo "i686-pc-windows-gnu" ;;
*) die "unsupported Windows arch under MSYS: $arch" ;;
esac
;;
*) die "unsupported platform: $os/$arch" ;;
esac
}
normalize_version() {
local v="$1"
v="${v#v}"
printf '%s' "$v"
}
parse_spec() {
local raw="$1"
SPEC_RAW="$raw"
SPEC_CHANNEL=""
SPEC_VERSION=""
SPEC_FLOATING=0
case "$raw" in
"") die "missing version/channel spec" ;;
stable | dev | night)
SPEC_CHANNEL="$raw"
SPEC_VERSION=""
SPEC_FLOATING=1
;;
*@*)
SPEC_VERSION="$(normalize_version "${raw%@*}")"
SPEC_CHANNEL="${raw#*@}"
case "$SPEC_CHANNEL" in
stable | dev) ;;
night) die "cannot pin a version on night (use: dcrup install night)" ;;
*) die "unknown channel in spec: $SPEC_CHANNEL (use stable|dev)" ;;
esac
[[ -n "$SPEC_VERSION" ]] || die "empty version in spec: $raw"
;;
*)
SPEC_VERSION="$(normalize_version "$raw")"
SPEC_CHANNEL="stable"
[[ -n "$SPEC_VERSION" ]] || die "invalid spec: $raw"
;;
esac
}
toolchain_id() {
if [[ "$SPEC_CHANNEL" == "night" ]]; then
local sha="${1:-unknown}"
echo "night-${sha}"
elif [[ "$SPEC_FLOATING" -eq 1 ]]; then
local ver="${1:-latest}"
echo "${SPEC_CHANNEL}-${ver}"
else
echo "${SPEC_VERSION}-${SPEC_CHANNEL}"
fi
}
ensure_dirs() {
mkdir -p "$DCRUP_TC" "$DCRUP_BIN" "$DCRUP_META"
}
active_file() { echo "$DCRUP_META/active"; }
read_active() {
if [[ -f "$(active_file)" ]]; then
cat "$(active_file)"
fi
}
write_active() {
printf '%s\n' "$1" >"$(active_file)"
}
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)"
}
fetch_json() {
need_cmd curl
curl -fsSL "$1"
}
json_tag() {
printf '%s' "$1" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\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() {
fetch_json "$API_LATEST"
}
latest_dev_json() {
need_cmd curl
local json result
json="$(fetch_json "$API_ALL")"
if command -v jq >/dev/null 2>&1; then
result="$(printf '%s' "$json" | jq -e -c '[.[] | select(.prerelease == true)][0]')" || true
elif command -v python3 >/dev/null 2>&1; then
result="$(printf '%s' "$json" | python3 -c '
import sys, json
rels = json.load(sys.stdin)
pre = [r for r in rels if r.get("prerelease")]
print(json.dumps(pre[0]) if pre else "", end="")
')"
else
die "dev channel needs jq or python3 to parse GitHub API"
fi
[[ -n "$result" && "$result" != "null" ]] || die "no prerelease (dev) found"
printf '%s' "$result"
}
release_json_for_spec() {
if [[ "$SPEC_FLOATING" -eq 1 ]]; then
case "$SPEC_CHANNEL" in
stable) latest_stable_json ;;
dev) latest_dev_json ;;
night) die "night has no GitHub release binary" ;;
esac
else
fetch_json "https://api.github.com/repos/dexoron/dcr/releases/tags/v${SPEC_VERSION}"
fi
}
install_prebuilt() {
local triple asset tag version id dest mirror
need_cmd curl
triple="$(detect_triple)"
json="$(release_json_for_spec)"
tag="$(json_tag "$json")"
[[ -n "$tag" ]] || die "failed to parse release tag"
version="$(normalize_version "$tag")"
asset="dcr-${triple}-${version}"
case "$triple" in
*windows*) asset="${asset}.exe" ;;
esac
id="$(toolchain_id "$version")"
dest="$DCRUP_TC/$id"
if [[ -x "$dest/dcr" && "${FORCE:-0}" != "1" ]]; then
ok "already installed: $id"
link_default "$id"
return 0
fi
mkdir -p "$dest"
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" \
"version=$version" \
"tag=$tag" \
"source=release" \
"triple=$triple" \
"mirror=$mirror" \
"libc=${DCRUP_LIBC:-gnu}" \
>"$dest/dcrup-meta"
ok "installed prebuilt $id ($triple via $mirror)"
link_default "$id"
}
install_build() {
local tmp id dest sha version branch_or_tag
need_cmd git
need_cmd cargo
tmp="$(mktemp -d "${TMPDIR:-/tmp}/dcrup.XXXXXX")"
cleanup_tmp() { rm -rf "$tmp"; }
trap cleanup_tmp EXIT
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"
sha="$(git -C "$tmp/src" rev-parse --short HEAD)"
version="night"
id="$(toolchain_id "$sha")"
elif [[ "$SPEC_FLOATING" -eq 1 ]]; then
local json tag
json="$(release_json_for_spec)"
tag="$(json_tag "$json")"
[[ -n "$tag" ]] || die "failed to resolve floating channel tag"
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 "$REPO_URL" "$tmp/src"
git -C "$tmp/src" fetch --depth 1 origin "refs/tags/${tag}:refs/tags/${tag}"
git -C "$tmp/src" checkout "$tag"
}
sha="$(git -C "$tmp/src" rev-parse --short HEAD)"
else
version="$SPEC_VERSION"
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 "$REPO_URL" "$tmp/src"
git -C "$tmp/src" fetch --depth 1 origin "refs/tags/${tag}:refs/tags/${tag}"
git -C "$tmp/src" checkout "$tag"
}
sha="$(git -C "$tmp/src" rev-parse --short HEAD)"
fi
dest="$DCRUP_TC/$id"
if [[ -x "$dest/dcr" && "${FORCE:-0}" != "1" ]]; then
ok "already installed: $id"
link_default "$id"
return 0
fi
log "building release (features: $FEATURES) …"
(
cd "$tmp/src"
# shellcheck disable=SC2086
cargo build --release --features $FEATURES
)
mkdir -p "$dest"
cp "$tmp/src/target/release/dcr" "$dest/dcr"
chmod +x "$dest/dcr"
printf '%s\n' "channel=$SPEC_CHANNEL" "version=$version" "git=$sha" "source=build" "features=$FEATURES" >"$dest/dcrup-meta"
ok "installed build $id"
link_default "$id"
trap - EXIT
cleanup_tmp
}
cmd_install() {
local mode="release" FORCE=0
local spec=""
local libc_cli=""
while [[ $# -gt 0 ]]; do
case "$1" in
--build)
mode="build"
shift
;;
--release)
mode="release"
shift
;;
--force)
FORCE=1
shift
;;
--libc)
[[ $# -ge 2 ]] || die "--libc needs gnu|musl|auto"
libc_cli="$2"
shift 2
;;
--libc=*)
libc_cli="${1#--libc=}"
shift
;;
--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"
spec="$1"
shift
;;
esac
done
[[ -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
if [[ -n "$libc_cli" ]]; then
case "$libc_cli" in
gnu | glibc | musl | auto) DCRUP_LIBC="$libc_cli" ;;
*) die "invalid --libc: $libc_cli (use gnu|musl|auto)" ;;
esac
fi
export DCRUP_LIBC="${DCRUP_LIBC:-gnu}"
if [[ "$SPEC_CHANNEL" == "night" ]]; then
mode="build"
fi
export FORCE
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
install_prebuilt
fi
check_path
}
cmd_default() {
[[ $# -eq 1 ]] || die "usage: dcrup default <spec>"
parse_spec "$1"
ensure_dirs
local id
if [[ "$SPEC_CHANNEL" == "night" || "$SPEC_FLOATING" -eq 1 ]]; then
id="$(ls -1 "$DCRUP_TC" 2>/dev/null | grep -E "^${SPEC_CHANNEL}-" | sort | tail -n1 || true)"
[[ -n "$id" ]] || die "no installed toolchain for channel $SPEC_CHANNEL (run: dcrup install $SPEC_CHANNEL)"
else
id="$(toolchain_id)"
[[ -d "$DCRUP_TC/$id" ]] || die "not installed: $id (run: dcrup install $1)"
fi
link_default "$id"
}
cmd_update() {
ensure_dirs
local active
active="$(read_active)"
[[ -n "$active" ]] || die "no active toolchain (run: dcrup install stable)"
case "$active" in
night-*)
log "updating night (rebuild HEAD) …"
FORCE=1 cmd_install night --build --force
;;
stable-* | dev-*)
local ch="${active%%-*}"
log "updating floating channel $ch …"
FORCE=1 cmd_install "$ch" --force
;;
*-stable | *-dev)
ok "pinned toolchain $active — not floating; install a newer version explicitly"
;;
*)
warn "unknown active id $active; try: dcrup install stable"
;;
esac
}
cmd_list() {
ensure_dirs
local active
active="$(read_active)"
if [[ ! -d "$DCRUP_TC" ]] || [[ -z "$(ls -A "$DCRUP_TC" 2>/dev/null || true)" ]]; then
warn "no toolchains installed"
return 0
fi
local d base
for d in "$DCRUP_TC"/*; do
[[ -d "$d" ]] || continue
base="$(basename "$d")"
if [[ "$base" == "$active" ]]; then
printf ' * %s\n' "$base"
else
printf ' %s\n' "$base"
fi
done
}
cmd_show() {
ensure_dirs
local active
active="$(read_active)"
echo "DCRUP_HOME=$DCRUP_HOME"
echo "DCRUP_BIN=$DCRUP_BIN"
echo "active=${active:-none}"
if [[ -n "$active" && -f "$DCRUP_TC/$active/dcrup-meta" ]]; then
echo "--- meta ---"
cat "$DCRUP_TC/$active/dcrup-meta"
fi
if [[ -x "$DCRUP_BIN/dcr" ]]; then
echo "--- dcr --version ---"
"$DCRUP_BIN/dcr" --version 2>/dev/null || true
fi
}
cmd_which() {
if [[ -L "$DCRUP_BIN/dcr" || -e "$DCRUP_BIN/dcr" ]]; then
readlink -f "$DCRUP_BIN/dcr" 2>/dev/null || readlink "$DCRUP_BIN/dcr" 2>/dev/null || echo "$DCRUP_BIN/dcr"
else
die "dcr not linked (run: dcrup install stable)"
fi
}
cmd_uninstall() {
ensure_dirs
if [[ "${1:-}" == "--all" ]]; then
rm -rf "$DCRUP_TC"
rm -f "$DCRUP_BIN/dcr" "$(active_file)"
ok "removed all toolchains"
return 0
fi
[[ $# -eq 1 ]] || die "usage: dcrup uninstall <spec>|--all"
parse_spec "$1"
local id
if [[ "$SPEC_FLOATING" -eq 1 || "$SPEC_CHANNEL" == "night" ]]; then
id="$(ls -1 "$DCRUP_TC" 2>/dev/null | grep -E "^${SPEC_CHANNEL}-" | sort | tail -n1 || true)"
else
id="$(toolchain_id)"
fi
[[ -n "$id" && -d "$DCRUP_TC/$id" ]] || die "not installed: $1"
rm -rf "$DCRUP_TC/$id"
if [[ "$(read_active)" == "$id" ]]; then
rm -f "$DCRUP_BIN/dcr" "$(active_file)"
warn "removed active toolchain; run: dcrup install stable"
fi
ok "removed $id"
}
check_path() {
case ":$PATH:" in
*":$DCRUP_BIN:"*) ;;
*)
warn "$DCRUP_BIN is not in PATH"
echo "Add to ~/.bashrc / ~/.zshrc / ~/.profile:"
echo " export PATH=\"$DCRUP_BIN:\$PATH\""
;;
esac
}
cmd_self_install() {
local dest="$DCRUP_BIN"
while [[ $# -gt 0 ]]; do
case "$1" in
--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 || 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"
check_path
echo "Then: dcrup install stable"
}
main() {
local cmd="${1:-}"
shift || true
case "$cmd" in
"" | -h | --help | help) usage ;;
install) cmd_install "$@" ;;
default) cmd_default "$@" ;;
update) cmd_update "$@" ;;
list) cmd_list "$@" ;;
show) cmd_show "$@" ;;
which) cmd_which "$@" ;;
uninstall) cmd_uninstall "$@" ;;
self-install) cmd_self_install "$@" ;;
*) die "unknown command: $cmd (try: dcrup help)" ;;
esac
}
main "$@"

View File

@@ -8,8 +8,11 @@ sidebar_label: Introduction
<summary>Quick reference</summary>
```bash
# Installation
curl -fsSL https://dcr.dexoron.su/install.sh | sh
# Installation (dcrup)
curl -fsSL https://sh.dcr-tool.ru | sh -s -- self-install
export PATH="$HOME/.dcr/bin:$PATH"
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,92 @@ sidebar_label: Changelog
# Changelog
## 0.9.5 (2026-10-21)
- **Structured CLI output** — shared helpers now provide Cargo-like human, plain, and JSON output modes.
- **Color and diagnostics policy** — `--color`, `NO_COLOR`, `--quiet`, and `--verbose` are applied consistently across commands.
- **Build reporting** — lifecycle events and compiler diagnostics are available as structured JSON events.
- **Output lab** — sandbox scenarios cover help, lifecycle commands, compiler warnings/errors, stream capture, and broken pipes.
## 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`.
`dcr new` / `dcr init` write the current version. On config load:
- missing / empty pin → warn "add dcr-version";
- dcr older than pin → warn "update dcr";
- dcr newer than pin → warn "check changelog, update pin";
- equal → silent.
Commands do not fail, warning only (once per path per process).
## 0.8.4 (2026-08-04)
**Added:**
- **`.dcr/` directory** — shared machine-readable build facts for IDE integrations:
- `.dcr/compile_commands.json`
- `.dcr/build-info.json` (`schemaVersion`, `artifact_path`, `profile`, …)
- `.dcr/toolchain.json` (compiler/debugger paths, moc/uic/rcc)
- `.dcr/ide/` (placeholder for IDE lock files)
- **`dcr gen project-info`** — `target`, `target_dir`, `artifact_path`, `artifact_kind`, `source_roots`, `include_globs`, `exclude_globs`; absolute/canonical paths.
- **`dcr build --print-artifact-path`** — one absolute artifact path line on stdout after a successful build.
- **`dcr gen … --quiet` / `-q`** — suppress human success logs for gen subcommands.
- **`.clangd`** — created on first gen with `CompilationDatabase: .dcr` (`# managed by dcr gen`); custom `.clangd` is not overwritten.
- **Helpers** `canonicalize_path`, `atomic_write`, `ensure_dcr_dir`; pure `resolve_artifact_path` via `platform::*`.
- **`dcr new` / `dcr init`** — add `.dcr/` to `.gitignore`.
**Changed (breaking):**
- **`compile_commands.json` moved** from project root to **`.dcr/compile_commands.json`**.
clangd discovers it via `.clangd` / `--compile-commands-dir=.dcr`.
CLion and VSCode generators updated accordingly.
- **Atomic write** for compile_commands (tmp + rename).
- **`gen vscode` / `gen clion` launch/run** — use the same artifact path resolver as `project-info` / `build-info.json`.
**Fixed:**
- Launch configs no longer hardcode `target/<profile>/<name>` without the Linux triple segment.
- Symlink/mount roots: best-effort canonicalize for root and compile database paths.
## 0.8.3 (2026-08-03)
### Added
- **`dcr run -- <args>...`** — cargo-style argument forwarding after a bare `--`
(`dcr run --release -- --test_help`). Applies to direct binary runs and `[run].cmd`.
- **Tests** — host target section `ldflags` without CLI `--target`; run-arg forwarding; `split_double_dash` unit tests.
### Changed
- **Native builds resolve the host triple** — without `--target` / package `build.target`, host sections such as
`[build.x86_64-unknown-linux-gnu]` apply again (`cflags` / `ldflags` / toolchain).
Previously the target was empty and those sections were ignored while artifacts already used the host triple path.
- **Empty target** — no more `Unknown target ''` warning.
- **`dcr run --help` / man `dcr-run`** — document `--` forwarding.
### Fixed
- **Native artifact paths on Windows and macOS** — without an explicit `--target` / `build.target`, artifacts use `target/<profile>`, so `dcr run` and flat-bin find the build output.
- Target-specific `ldflags` missing on native host builds (e.g. micro-lang + `libsct-elf`).
- Spurious empty-target warning.
## 0.8.2 (2026-07-22)
### Added

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

@@ -4,25 +4,75 @@ sidebar_label: Installation
# Installation
## Linux (Dexoron Packages)
## dcrup (recommended)
Configure your package manager at https://pkg.dexoron.su and install:
Install and switch DCR versions (stable / dev / night, pin `0.8.2`, optional build from source).
### Linux / macOS / BSD / Windows (bash)
**Arch Linux**
```sh
sudo pacman -Sy dcr
curl --proto '=https' --tlsv1.2 -sSf https://sh.dcr-tool.ru | sh -s -- self-install
export PATH="$HOME/.dcr/bin:$PATH"
dcrup install stable
```
**Debian/Ubuntu**
Non-interactive install of DCR in one shot:
```sh
sudo apt update && sudo apt install dcr
curl --proto '=https' --tlsv1.2 -sSf https://sh.dcr-tool.ru | sh -s -- install stable
export PATH="$HOME/.dcr/bin:$PATH"
```
**Fedora/RHEL**
```sh
sudo dnf install dcr
### Windows (PowerShell)
```powershell
& ([scriptblock]::Create((irm https://ps1.dcr-tool.ru))) self-install
# $env:Path += ";$env:USERPROFILE\.dcr\bin"
dcrup install stable
```
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 (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
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)
```sh
@@ -42,11 +92,12 @@ brew install dcr
sudo snap install dcrup
```
> If classic Snap Store publishing is unavailable, install the `.snap` from [GitHub Releases](https://github.com/dexoron/dcr/releases/latest) with `--dangerous`.
## Nix (flake)
```sh
nix run github:dexoron/dcr
# or install to profile:
nix profile install github:dexoron/dcr
```
@@ -56,48 +107,37 @@ nix profile install github:dexoron/dcr
cargo install dcr
```
## Linux / macOS (script)
Note: crates.io builds may omit optional features. For FAT disk images (`[archive]`), prefer release binaries or:
```sh
curl -fsSL https://dcr.dexoron.su/install.sh | sh
```
## BSD (script)
```sh
curl -fsSL https://dcr.dexoron.su/install_bsd.sh | sh
```
## Windows (PowerShell)
```powershell
irm https://dcr.dexoron.su/install.ps1 | iex
cargo install dcr --features archive
```
## From source
```sh
git clone https://github.com/dexoron/dcr.git
cd dcr && cargo build --release
cd dcr
cargo build --release --features archive
ln -sf "$PWD/target/release/dcr" ~/.local/bin/dcr
# or manage versions with dcrup install night / --build
```
## Post-install
Verify the installation:
```bash
dcr --version
dcrup show # if installed via dcrup
```
Man pages are available after installation:
Man pages (package installs / release assets):
```bash
man dcr
man dcr-build
```
To use the registry, create `~/.dcr/config.toml`:
Registry (optional) — `~/.dcr/config.toml`:
```toml
[registry.default]

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

@@ -0,0 +1,34 @@
---
sidebar_label: CLI output
---
# CLI output (experimental)
The structured output implementation is being developed incrementally.
Currently these global options apply to diagnostics emitted through the logger:
```sh
dcr --output plain build
dcr --output json build
dcr --color never build
dcr --quiet build
dcr --verbose build
```
`--output` accepts `human` (default), `plain`, or `json`.
Diagnostics are written to stderr. JSON diagnostics are newline-delimited objects
with `schema_version: 1`, `event`, and `message` fields. Command statuses use the
shared output helpers, build reporting emits structured lifecycle events, and
compiler output is represented as JSON events with its original stream and text.
The protocol is still experimental: some raw command data and subprocess stdout
remain unstructured.
`--color` accepts `auto`, `always`, or `never`. Automatic coloring requires a
terminal and respects `NO_COLOR`. Plain and JSON diagnostics never add colors.
Quiet mode preserves warnings, errors, and compiler diagnostics; verbose mode
enables debug and trace logging. Arguments following `--` are passed through
unchanged.
Remaining work includes migrating the remaining raw command data and subprocess
stdout, improving progress rendering and contextual errors, and expanding
integration coverage for stdout/stderr and closed pipes.

View File

@@ -43,7 +43,8 @@ Main project configuration file. Located at the project root.
| Field | Required | Description |
|-------|----------|-------------|
| `name` | yes | Project name |
| `version` | yes | Semantic version |
| `version` | yes | Semantic version of the **project** |
| `dcr-version` | no | Semver of the **dcr tool** this project targets. Written by `dcr new` / `dcr init` to the current tool version. |
| `type` | no | `app`, `lib`, `none` (defaults to `"none"`) |
| `license` | no | SPDX license identifier |
| `author` | no | Author |
@@ -52,11 +53,26 @@ Main project configuration file. Located at the project root.
[package]
name = "my-app"
version = "0.1.0"
dcr-version = "0.8.4"
type = "app"
license = "MIT"
author = "John Doe"
```
### `package.dcr-version`
Optional pin of the DCR CLI the project was written for. On every `dcr.toml` load:
| Situation | Behaviour |
|-----------|-----------|
| Field missing / empty | Warning: add `dcr-version` (set by `dcr new` / `dcr init`) |
| Running dcr **older** than pin | Warning: upgrade dcr (`dcr --update`) |
| Running dcr **newer** than pin | Warning: review changelog, bump the pin when ready |
| Equal | Silent |
| Invalid semver | Warning once, field ignored |
Never fails the command — warnings only. Pre-release suffixes (`1.0.0-dev`) are ignored for comparison (`1.0.0-dev` ≡ `1.0.0`).
## [build]
...
@@ -95,7 +111,11 @@ author = "John Doe"
| `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-BUILD 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
.TH DCR-BUILD 1 "2026-08-04" "dcr 0.8.4" "DCR Manual"
.SH NAME
dcr-build \- Compile a DCR project
.SH SYNOPSIS
@@ -9,8 +9,15 @@ dcr-build \- Compile a DCR project
[\fB\-\-clean\fR]
[\fB\-\-verbose\fR]
[\fB\-\-workspace\fR \fI<name>\fR]
[\fB\-\-print\-artifact\-path\fR]
.SH DESCRIPTION
Compiles the project. Default profile is \fB\-\-debug\fR.
Compiles the project. Default profile is
.B \-\-debug .
On success, updates
.B .dcr/build-info.json
and
.B .dcr/toolchain.json
when applicable.
.SH OPTIONS
.TP
.B \-\-debug
@@ -33,5 +40,9 @@ Print detailed build output including compiler commands.
.TP
.B \-\-workspace \fI<name>\fR
Build a specific workspace member.
.TP
.B \-\-print\-artifact\-path
After a successful build, print the absolute path of the main artifact as a single line on stdout.
.SH SEE ALSO
.BR dcr (1)
.BR dcr (1),
.BR dcr-gen (1)

View File

@@ -1,23 +1,67 @@
.TH DCR-GEN 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
.TH DCR-GEN 1 "2026-08-04" "dcr 0.8.4" "DCR Manual"
.SH NAME
dcr-gen \- Generate IDE and tooling integration files
.SH SYNOPSIS
.B dcr gen \fI<subcommand>\fR
[\fB\-\-debug\fR | \fB\-\-release\fR]
[\fB\-\-quiet\fR | \fB\-q\fR]
.SH DESCRIPTION
Generates IDE and tooling integration files for DCR projects.
Machine-readable build facts are written under
.B .dcr/
(compile database, build-info, toolchain).
.SH SUBCOMMANDS
.TP
.B project-info
Print project metadata as JSON to stdout.
Print project metadata as JSON to stdout (absolute paths, artifact_path, target_dir).
Also updates
.B .dcr/build-info.json
and
.B .dcr/toolchain.json .
.TP
.B compile-commands
Generate \fBcompile_commands.json\fR for IDE tooling.
Generate
.B .dcr/compile_commands.json
(atomic write) and ensure
.B .clangd
points at
.B .dcr
as CompilationDatabase.
.TP
.B vscode
Generate \fB.vscode/\fR integration files (tasks, launch configurations).
Generate
.B .vscode/
integration files (tasks, launch configurations) and compile commands under
.B .dcr/ .
.TP
.B clion
Generate \fB.idea/\fR integration files for CLion.
Generate
.B .idea/
integration files for CLion and compile commands under
.B .dcr/ .
.SH OPTIONS
.TP
.B \-\-debug
Use debug profile (default).
.TP
.B \-\-release
Use release profile.
.TP
.B \-\-quiet\fR, \fB\-q
Suppress human-readable success messages on stdout.
.SH FILES
.TP
.B .dcr/compile_commands.json
JSON Compilation Database (moved from project root; breaking change in 0.8.4).
.TP
.B .dcr/build-info.json
Last known artifact path, profile, compiler.
.TP
.B .dcr/toolchain.json
Resolved compiler/debugger (and Qt tools if configured).
.TP
.B .clangd
Created once with CompilationDatabase: .dcr; not overwritten if customized.
.SH SEE ALSO
.BR dcr (1),
.BR dcr-build (1)

View File

@@ -8,9 +8,13 @@ dcr-run \- Build and run a DCR project
[\fB\-\-force\fR]
[\fB\-\-clean\fR]
[\fB\-\-verbose\fR]
[\fB\-\-\fR \fI<args>...\fR]
.SH DESCRIPTION
Builds and runs the project. Only available for projects with
\fBkind = "bin"\fR.
Arguments after a bare
.B \-\-
are forwarded to the built binary (cargo-style).
.SH OPTIONS
.TP
.B \-\-debug
@@ -30,6 +34,14 @@ Clean before building.
.TP
.B \-\-verbose
Print detailed build output.
.TP
.B \-\- \fI<args>...\fR
Arguments passed to the built binary.
.SH EXAMPLES
.nf
dcr run --release -- --test_help
dcr run -- --foo bar
.fi
.SH SEE ALSO
.BR dcr (1),
.BR dcr-build (1)

View File

@@ -1,4 +1,4 @@
.TH DCR 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
.TH DCR 1 "2026-10-21" "dcr 0.9.5" "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
@@ -128,7 +131,7 @@ Version from registry (e.g. \fB1.2.3\fR).
Print project metadata as JSON to stdout.
.TP
.B compile-commands
Generate \fBcompile_commands.json\fR for IDE tooling.
Generate \fB.dcr/compile_commands.json\fR for IDE tooling.
.TP
.B vscode
Generate \fB.vscode/\fR integration files (tasks, launch configurations).
@@ -154,6 +157,11 @@ Project name (required).
.B package.version
Project version (default: \fB0.1.0\fR).
.TP
.B package.dcr\-version
Semver of the DCR tool this project targets. Set automatically by
\fBdcr new\fR / \fBdcr init\fR. Missing pin, or an installed \fBdcr\fR older/newer
than the pin, prints a warning (commands still run).
.TP
.B build.language
Programming language: \fBc\fR or \fBc++\fR (default: \fBc\fR).
.TP

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,46 +15,64 @@
// 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::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::prelude::*;
use crate::core::{build_config::Config, deps::register};
use crate::utils::fs::find_project_root;
use toml::{Value, map::Map};
/// Parsed arguments for the `dcr add` command.
///
/// 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`.
pub name: String,
/// Local filesystem path when the source uses the `path:` prefix.
pub path: Option<String>,
/// Git repository URL when the source is a git/GitHub/GitLab URL or prefix.
pub git: Option<String>,
/// Optional git branch to pin the dependency to.
pub branch: Option<String>,
/// Optional git tag to pin the dependency to.
pub tag: Option<String>,
/// Optional git commit hash (revision) to pin the dependency to.
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`.
///
/// Parses CLI arguments, locates the project root, builds a TOML dependency value
/// (path, git with optional ref, or registry version), and writes it under
/// `dependencies.<name>`.
///
/// # Parameters
/// - `args`: Tokens after `dcr add` (name, source, optional `--branch`/`--tag`/`--rev`).
///
/// # Returns
/// 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,
);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Adds a dependency to the project.");
println!();
printc("SOURCES:", BOLD_GREEN);
println!(" github:user/repo GitHub repository");
println!(" gitlab:user/repo GitLab repository");
println!(" git:host.com/user/repo Generic git repository");
println!(" path:./path/to/lib Local path dependency");
println!(" <version> Version from registry");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --branch <b> Use a specific branch");
println!(" --tag <t> Use a specific tag");
println!(" --rev <r> Use a specific commit hash");
let mut help = crate::utils::help::HelpBuilder::new("dcr add");
help.section("USAGE")
.example("dcr add <name> <source> [--branch <b>] [--tag <t>] [--rev <r>]");
help.description("Adds a dependency to the project.");
help.section("SOURCES")
.item("github:user/repo", "GitHub repository")
.item("gitlab:user/repo", "GitLab repository")
.item("git:host.com/user/repo", "Generic git repository")
.item("path:./path/to/lib", "Local path dependency")
.item("pkg-config:fmt", "System pkg-config dependency")
.item("<version>", "Version from registry");
help.section("OPTIONS")
.item("--branch <b>", "Use a specific branch")
.item("--tag <t>", "Use a specific tag")
.item("--rev <r>", "Use a specific commit hash");
help.render();
return 0;
}
@@ -66,7 +84,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;
}
};
@@ -74,11 +92,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;
}
};
@@ -86,11 +104,12 @@ 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;
}
};
// Build the TOML value written under dependencies.<name>
let dep_value = if let Some(git_url) = add_args.git {
if add_args.branch.is_none() && add_args.tag.is_none() && add_args.rev.is_none() {
let mut table = Map::new();
@@ -110,6 +129,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));
@@ -117,32 +147,34 @@ 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),
add_args.name
);
crate::utils::output::success(format!("Added dependency `{}` to dcr.toml", add_args.name));
0
}
/// Parses CLI arguments into an [`AddArgs`] value.
///
/// Expects `<name> [source] [--branch|--tag|--rev ...]`. If `source` is omitted,
/// the package registry is queried for a default version. Source strings may use
/// `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);
}
@@ -155,7 +187,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();
@@ -163,12 +195,12 @@ 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();
} else {
// No source provided — try registry auto-lookup
// No source provided — resolve version from the package registry
match register::resolve_package_from_registry(&name) {
Ok(info) => {
let version = info
@@ -182,7 +214,7 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
)
})
.map_err(|(msg, code)| {
error(&msg);
error!("{msg}");
code
})?;
return Ok(AddArgs {
@@ -193,10 +225,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);
}
}
@@ -207,26 +240,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);
}
}
@@ -234,8 +267,25 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
let mut path = None;
let mut git = None;
let mut pkg_config = None;
if let Some(p) = source_spec.strip_prefix("path:") {
// Map source prefixes / bare URLs to path or git fields
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));
@@ -243,10 +293,10 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
git = Some(format!("https://gitlab.com/{}", g));
} else if let Some(g) = source_spec.strip_prefix("git:") {
if g.contains('/') && !g.contains('.') {
// git:user/repo -> default to github
// git:user/repo with no host — default to GitHub
git = Some(format!("https://github.com/{}", g));
} else {
// git:host.com/user/repo
// git:host.com/user/repo or an already-qualified URL
let url = if g.starts_with("http") || g.starts_with("git@") {
g.to_string()
} else {
@@ -259,8 +309,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);
}
@@ -272,5 +329,6 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
tag,
rev,
version_from_registry: None,
pkg_config,
})
}

View File

@@ -15,32 +15,35 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::prelude::*;
use crate::cli::clean::clean;
use crate::cli::flags::parse_build_run_flags;
use crate::cli::r#gen::{ProjectInfo, write_dcr_metadata};
use crate::core::build::builder::artifact::{absolute_artifact_path, resolve_artifact_path};
use crate::core::build::common;
use crate::core::build::{BuildEvent, BuildReporter, BuildRequest, run_build};
use crate::core::build_config::Config;
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::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::fs::{canonicalize_path, find_project_root};
use crate::utils::output;
use std::io::IsTerminal;
use std::path::Path;
use std::sync::{Arc, atomic::AtomicBool};
pub use crate::core::build::get_build_string_with_profile;
fn status(verb: &str, style: &str) -> String {
colored(&format!("{verb:<9}"), style)
}
/// CLI build reporter that prints human-readable progress to stderr/stdout.
struct CliReporter;
impl CliReporter {
fn line(&self, verb: &str, style: &str, rest: &str) {
eprintln!(" {} {}", status(verb, style), rest);
}
}
impl BuildReporter for CliReporter {
/// Handles build events by printing progress, status lines, and compiler output.
///
/// # Parameters
/// - `event`: Build pipeline event to report to the user.
fn on_event(&mut self, event: BuildEvent<'_>) {
match event {
BuildEvent::TargetStart {
@@ -49,7 +52,13 @@ impl BuildReporter for CliReporter {
target,
} => {
common::finish_progress_line();
self.line("target", BOLD_CYAN, &format!("{index}/{total} {target}"));
output::emit_event(
"target_started",
serde_json::json!({
"index": index, "total": total, "target": target
}),
&format!("target {index}/{total} {target}"),
);
}
BuildEvent::ProjectStart {
name,
@@ -57,15 +66,23 @@ impl BuildReporter for CliReporter {
target,
} => {
common::finish_progress_line();
self.line(
"project",
BOLD_CYAN,
&format!("{name} ({profile}, {target})"),
output::emit_event(
"project_started",
serde_json::json!({
"name": name, "profile": profile, "target": target
}),
&format!("project {name} ({profile}, {target})"),
);
}
BuildEvent::DepBuilding { name, version } => {
common::finish_progress_line();
self.line("dep", BOLD_YELLOW, &format!("{name} v{version}"));
output::emit_event(
"dependency_building",
serde_json::json!({
"name": name, "version": version
}),
&format!("dep {name} v{version}"),
);
}
BuildEvent::DepReady {
name,
@@ -73,60 +90,95 @@ impl BuildReporter for CliReporter {
rebuilt,
} => {
common::finish_progress_line();
// Only announce deps that were actually rebuilt this run.
if rebuilt {
self.line("ready", BOLD_GREEN, &format!("{name} v{version}"));
output::emit_event(
"dependency_ready",
serde_json::json!({
"name": name, "version": version, "rebuilt": rebuilt
}),
&format!("ready {name} v{version}"),
);
}
}
BuildEvent::Compiling { name, version } => {
common::finish_progress_line();
let label = format!(" {} {} v{}", status("compile", BOLD_GREEN), name, version);
common::set_progress_label(Some(label.clone()));
if !std::io::stderr().is_terminal() {
eprintln!("{label}");
let label = format!(
" {}",
output::progress_label("compile", &format!("{name} v{version}"))
);
// Terminals use an in-place progress line; non-TTY gets a plain log line.
if output::mode() == output::Mode::Human {
common::set_progress_label(Some(label.clone()));
}
if output::mode() != output::Mode::Human || !std::io::stderr().is_terminal() {
output::emit_event(
"compiling",
serde_json::json!({
"name": name, "version": version
}),
&format!("compile {name} v{version}"),
);
}
}
BuildEvent::Packing { path } => {
common::finish_progress_line();
self.line("pack", BOLD_CYAN, path);
output::emit_event(
"packing",
serde_json::json!({ "path": path }),
&format!("pack {path}"),
);
}
BuildEvent::Finished { secs } => {
common::finish_progress_line();
self.line("done", BOLD_GREEN, &format!("in {secs}s"));
output::emit_event(
"finished",
serde_json::json!({ "seconds": secs }),
&format!("done in {secs}s"),
);
}
BuildEvent::CompilerOutput { stream, text } => {
// Pause the progress spinner so compiler text is not overwritten.
common::interrupt_progress_for_output();
if stream == "stderr" {
eprint!("{text}");
} else {
print!("{text}");
}
output::compiler_output(stream, text);
}
}
}
}
/// Runs the project build command from the CLI.
///
/// Parses flags, locates the project root, optionally cleans, then invokes the
/// core build pipeline. On success, may write `.dcr` metadata and print the
/// main artifact path when requested.
///
/// # Parameters
/// - `args`: Tokens after `dcr build` (profile, target, force, clean, etc.).
///
/// # Returns
/// 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]",
BOLD_CYAN,
);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Compiles the project. Default profile is --debug.");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --debug Build with debug profile (default)");
println!(" --release Build with release profile");
println!(" --target <triple> Cross-compile for the given target");
println!(" --force Force a full rebuild");
println!(" --clean Clean before building");
println!(" --verbose Print detailed build output");
println!(" --workspace <name> Build a specific workspace member");
let mut help = crate::utils::help::HelpBuilder::new("dcr build");
help.description("Compiles the project. Default profile is --debug.");
help.section("USAGE").example("dcr build [options]");
help.section("OPTIONS")
.item("--debug", "Build with debug profile (default)")
.item("--release", "Build with release profile")
.item("--target <triple>", "Cross-compile for the given target")
.item("--force", "Force a full rebuild")
.item("--clean", "Clean before building")
.item("--verbose", "Print detailed build output")
.item("--workspace <name>", "Build a specific workspace member")
.item(
"--print-artifact-path",
"Print the absolute main artifact path",
);
help.render();
return 0;
}
// Shared cancel flag so Ctrl-C can stop an in-progress build cooperatively.
let cancel = Arc::new(AtomicBool::new(false));
let cancel_clone = cancel.clone();
ctrlc::set_handler(move || {
@@ -136,18 +188,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)) => 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;
}
};
@@ -156,6 +208,7 @@ pub fn build(args: &[String]) -> i32 {
Err(code) => return code,
};
// Prefer CLI --target; otherwise fall back to profile target from dcr.toml.
if flags.target.is_none() {
let config_path = root.join("dcr.toml");
if let Ok(config) = Config::open(config_path.to_str().unwrap())
@@ -174,20 +227,183 @@ pub fn build(args: &[String]) -> i32 {
}
let req = BuildRequest {
profile: flags.profile,
target: flags.target,
profile: flags.profile.clone(),
target: flags.target.clone(),
force: flags.force,
verbose: flags.verbose,
workspace: flags.workspace,
workspace: flags.workspace.clone(),
cancel,
};
let mut reporter = CliReporter;
match run_build(&root, &req, &mut reporter) {
Ok(_) => 0,
Ok(_) => {
// Persist IDE/metadata and optionally echo the main artifact path.
if let Some(info) =
resolve_post_build_info(&root, &flags.profile, flags.target.as_deref())
{
let _ = write_dcr_metadata(&root, &info);
if flags.print_artifact_path
&& let Some(ref path) = info.artifact_path
{
if crate::utils::output::mode() == crate::utils::output::Mode::Json {
crate::utils::output::write_stdout_raw(&format!("{path}\n"));
} else {
crate::say!("{path}");
}
}
} else if flags.print_artifact_path
&& let Some(path) =
fallback_artifact_path(&root, &flags.profile, flags.target.as_deref())
{
if crate::utils::output::mode() == crate::utils::output::Mode::Json {
crate::utils::output::write_stdout_raw(&format!("{path}\n"));
} else {
crate::say!("{path}");
}
}
0
}
Err(err) => {
common::finish_progress_line();
error(&err.message);
error!("{}", err.message);
1
}
}
}
/// Collects project and artifact metadata after a successful build.
///
/// Reads `dcr.toml`, resolves language/compiler/kind/target/output settings for
/// the given profile, and computes absolute artifact and target directory paths.
///
/// # Parameters
/// - `root`: Project root with `dcr.toml`.
/// - `profile`: Build profile used for the just-finished build.
/// - `cli_target`: Optional target from the CLI (overrides config when set).
///
/// # Returns
/// `Some(ProjectInfo)` for a normal package; `None` for workspace-only roots
/// or when required config/paths cannot be resolved.
fn resolve_post_build_info(
root: &Path,
profile: &str,
cli_target: Option<&str>,
) -> Option<ProjectInfo> {
let config = Config::open(root.join("dcr.toml").to_str()?).ok()?;
if config.is_workspace_only() {
return None;
}
let name = get_config_str(&config, "package.name");
let version = get_config_str(&config, "package.version");
let language = get_language_with_profile_or_default(&config, profile);
let standard = get_string_with_profile(&config, "standard", profile);
let cxx_standard = get_string_with_profile(&config, "cxx_standard", profile);
let compiler_s = get_string_with_profile(&config, "compiler", profile);
let kind = normalize_kind(&get_string_with_profile(&config, "kind", profile)).to_string();
let build_target = cli_target
.map(|s| s.to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| get_string_with_profile(&config, "target", profile));
let output_filename_s = get_string_with_profile(&config, "filename", profile);
let output_extension_s = get_string_with_profile(&config, "extension", profile);
let output_filename = if output_filename_s.is_empty() {
None
} else {
Some(output_filename_s)
};
let output_extension = if output_extension_s.is_empty() {
None
} else {
Some(output_extension_s)
};
let tc_cc = get_config_opt(&config, "toolchain.cc");
let tc_cxx = get_config_opt(&config, "toolchain.cxx");
let tc_as = get_config_opt(&config, "toolchain.as");
let resolved_compiler = resolve_compiler(
&language,
&compiler_s,
tc_cc.as_deref(),
tc_cxx.as_deref(),
tc_as.as_deref(),
);
let out_dir = get_string_with_profile(&config, "out_dir", profile);
// Explicit target (CLI or config) changes how the artifact directory is laid out.
let has_explicit = cli_target.is_some_and(|t| !t.is_empty()) || !build_target.trim().is_empty();
let target_dir =
resolve_artifact_target_dir(root, None, profile, &build_target, &out_dir, has_explicit);
let rel = resolve_artifact_path(
&kind,
profile,
&name,
Some(&target_dir),
output_filename.as_deref(),
output_extension.as_deref(),
)?;
let artifact_path = absolute_artifact_path(root, &rel)
.to_string_lossy()
.to_string();
let abs_target_dir = {
let p = Path::new(&target_dir);
let path = if p.is_absolute() {
canonicalize_path(p)
} else {
canonicalize_path(&root.join(p))
};
path.to_string_lossy().into_owned()
};
Some(ProjectInfo {
name,
version,
root: root.to_path_buf(),
profile: profile.to_string(),
language,
standard,
cxx_standard,
compiler: resolved_compiler,
kind: kind.clone(),
target: build_target,
target_dir: Some(abs_target_dir),
artifact_path: Some(artifact_path),
artifact_kind: kind,
sources: vec![],
include_dirs: vec![],
lib_dirs: vec![],
libs: vec![],
cflags: vec![],
ldflags: vec![],
source_roots: vec![],
include_globs: vec![],
exclude_globs: vec![],
output_filename,
output_extension,
out_dir,
debugger: "lldb".to_string(),
moc: None,
uic: None,
rcc: None,
workspace_root: None,
})
}
/// Best-effort absolute main artifact path when full post-build info is unavailable.
///
/// Assumes a binary (`"bin"`) kind and uses the package name plus profile,
/// target, and `out_dir` settings from config.
fn fallback_artifact_path(root: &Path, profile: &str, target: Option<&str>) -> Option<String> {
let config = Config::open(root.join("dcr.toml").to_str()?).ok()?;
let name = get_config_str(&config, "package.name");
if name.is_empty() {
return None;
}
let build_target = target.unwrap_or("").to_string();
let out_dir = get_string_with_profile(&config, "out_dir", profile);
let has_explicit = !build_target.trim().is_empty();
let target_dir =
resolve_artifact_target_dir(root, None, profile, &build_target, &out_dir, has_explicit);
let rel = resolve_artifact_path("bin", profile, &name, Some(&target_dir), None, None)?;
Some(
absolute_artifact_path(root, &rel)
.to_string_lossy()
.to_string(),
)
}

View File

@@ -15,76 +15,89 @@
// 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::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 glob::glob;
use std::fs;
use std::path::Path;
use std::{fs, path::Path};
/// Entry point for the `dcr clean` command.
///
/// Removes build artifacts from the project (and optionally workspace members).
///
/// # Parameters
/// - `args`: Tokens after `dcr clean` (`--debug`/`--release`, `--target`, `--all`).
///
/// # Returns
/// 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,
);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Removes build artifacts from the target directory.");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --debug Clean debug artifacts (default)");
println!(" --release Clean release artifacts");
println!(" --target <triple> Clean artifacts for a specific target");
println!(" --all Clean all workspace members");
let mut help = crate::utils::help::HelpBuilder::new("dcr clean");
help.section("USAGE")
.example("dcr clean [--debug | --release] [--target <triple>] [--all]");
help.description("Removes build artifacts from the target directory.");
help.section("OPTIONS")
.item("--debug", "Clean debug artifacts (default)")
.item("--release", "Clean release artifacts")
.item("--target <triple>", "Clean artifacts for a specific target")
.item("--all", "Clean all workspace members");
help.render();
return 0;
}
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;
}
};
// Run cleanup relative to the project root so paths resolve consistently.
match with_dir(&root, || clean_from_root(&root, &flags)) {
Ok(()) => 0,
Err(msg) => {
error(&msg);
error!("{msg}");
1
}
}
}
/// Parsed flags controlling which artifacts `clean` should remove.
struct CleanFlags {
/// Optional build profile name (e.g. `debug`, `release`).
profile: Option<String>,
/// Optional target triple to clean under `target/<triple>/...`.
target: Option<String>,
/// When true, also clean each workspace member that has a `target` directory.
all: bool,
}
/// Parses CLI arguments into [`CleanFlags`].
///
/// Recognizes `--all`, `--target <triple>`, and profile flags such as `--debug`
/// / `--release` (validated via [`default_profile_flags`]).
fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
let mut profile: Option<String> = None;
let mut target: Option<String> = None;
@@ -104,6 +117,7 @@ fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
continue;
}
if arg.starts_with("--") {
// Accept only known profile names (e.g. debug/release), not arbitrary flags.
let candidate = arg.trim_start_matches("--").to_string();
if !default_profile_flags(&candidate).is_empty() {
if profile.is_some() {
@@ -122,20 +136,35 @@ fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
})
}
/// Cleans the project at `root`, and optionally all workspace members.
///
/// Resolves the effective target triple from flags, `dcr.toml`, or host defaults,
/// then removes the appropriate `target` subdirectories and custom clean paths.
fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
let target = flags
.target
.clone()
.or_else(|| {
config
.get("build.target")
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
})
.or_else(|| Some(default_target_triple()));
// Prefer CLI --target, then build.target from config, then host defaults on Linux/BSD.
let target = flags.target.clone().or_else(|| {
config
.get("build.target")
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
});
let target = match target {
Some(t) => Some(crate::utils::build::normalize_target_os(&t).to_string()),
None if cfg!(target_os = "linux")
|| cfg!(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)) =>
{
Some(default_target_triple())
}
None => None,
};
if flags.all
&& let Some(workspace) = parse_workspace(
@@ -155,6 +184,12 @@ fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
clean_project_at(root, flags.profile.as_deref(), target.as_deref())
}
/// Removes build artifacts for a single project directory.
///
/// When `profile` is set, only that profile's directory under `target` (optionally
/// scoped by `target` triple) is removed, plus custom paths for that profile.
/// When `profile` is `None`, the entire `target` tree is removed and custom paths
/// are cleaned for both `debug` and `release`.
fn clean_project_at(
project_root: &Path,
profile: Option<&str>,
@@ -170,10 +205,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)
);
crate::utils::output::status("Cleaning", format!("project `{project_name}`"));
if let Some(profile) = profile {
let target_dir = if let Some(t) = target {
format!("target/{t}/{profile}")
@@ -182,24 +214,21 @@ fn clean_project_at(
};
let target_items = check_dir(Some("target")).unwrap_or_default();
let parent_dir = target.unwrap_or("");
// Existence check differs: with a triple, look for target/<triple>; otherwise target/<profile>.
let dir_exists = if target.is_some() {
target_items.contains(&parent_dir.to_string())
} else {
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));
crate::utils::output::status("Profile", profile);
if let Some(t) = target {
println!(" Target: {}", colored(t, BOLD_GREEN));
crate::utils::output::status("Target", t);
}
let _ = fs::remove_dir_all(&target_dir);
println!(
"{} Removed directory {}",
colored("\n ✔", BOLD_GREEN),
target_dir
);
crate::utils::output::removed(format!("directory {target_dir}"));
}
clean_custom_paths(&config, profile)?;
return Ok(());
@@ -207,12 +236,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)
);
crate::utils::output::removed("directory target");
} else {
warn("Directory target not found");
warn!("Directory target not found");
}
clean_custom_paths(&config, "debug")?;
clean_custom_paths(&config, "release")?;
@@ -220,6 +246,10 @@ fn clean_project_at(
})
}
/// Deletes extra paths listed under `build.clean` in `dcr.toml` for a profile.
///
/// Each pattern may contain `{profile}` and version placeholders, which are
/// expanded before matching with `glob`. Matched files and directories are removed.
fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> {
let patterns = match config.get("build.clean") {
Some(v) => v
@@ -250,6 +280,10 @@ fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> {
Ok(())
}
/// Expands version-related placeholders in a clean path pattern.
///
/// Reads `package.version` from `config`, parses it, and applies
/// [`crate::utils::build::substitute_version_vars`].
fn substitute_clean_vars(value: &str, config: &Config) -> String {
let version = config
.get("package.version")

View File

@@ -15,8 +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::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use crate::prelude::*;
use serde::Deserialize;
use std::fs;
use std::io::Read;
@@ -27,45 +27,60 @@ use std::time::{SystemTime, UNIX_EPOCH};
const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/dexoron/dcr/releases/latest";
/// Metadata for the latest GitHub release of DCR.
#[derive(Debug, Deserialize)]
struct Release {
/// Release tag, typically including a leading `v` (e.g. `v1.2.3`).
tag_name: String,
/// Downloadable assets attached to the release.
assets: Vec<ReleaseAsset>,
}
/// A single downloadable asset from a GitHub release.
#[derive(Debug, Deserialize)]
struct ReleaseAsset {
/// Asset file name as published on GitHub.
name: String,
/// Direct URL used to download the asset.
browser_download_url: String,
}
/// Handles the `dcr --update` flag: checks GitHub for a newer release and
/// replaces the running binary when an update is available.
///
/// # Parameters
/// - `args`: Remaining tokens after `--update` (only `--help` is accepted).
///
/// # Returns
/// `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);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Updates DCR to the latest version from GitHub Releases.");
let mut help = crate::utils::help::HelpBuilder::new("dcr --update");
help.section("USAGE").example("dcr --update");
help.description("Updates DCR to the latest version from GitHub Releases.");
help.render();
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");
println!(
"Update via package manager: yay/paru -Syu {package_name} or sudo pacman -Syu {package_name}"
warn!("This dcr binary is managed by pacman/AUR");
crate::utils::output::status(
"Update via package manager:",
format!("yay/paru -Syu {package_name} or sudo pacman -Syu {package_name}"),
);
return 0;
}
@@ -76,14 +91,15 @@ 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;
}
};
// Normalize the tag by stripping a leading `v` before comparison.
let latest_version = release.tag_name.trim_start_matches('v');
if latest_version == current_version {
println!("Latest version is already installed: {current_version}");
crate::utils::output::status("Up to date", current_version);
return 0;
}
@@ -93,14 +109,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;
}
};
@@ -108,22 +124,26 @@ 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);
// 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;
}
let _ = fs::remove_file(&temp_path);
println!("Update completed: {current_version} -> {latest_version}");
crate::utils::output::success(format!(
"Updated DCR: {current_version} -> {latest_version}"
));
0
}
/// Fetches the latest DCR release metadata from the GitHub Releases API.
fn fetch_latest_release() -> Result<Release, String> {
let response = ureq::get(LATEST_RELEASE_URL)
.set("User-Agent", "dcr-updater")
@@ -138,6 +158,7 @@ fn fetch_latest_release() -> Result<Release, String> {
.map_err(|_| "GitHub API response has an unexpected format".to_string())
}
/// Downloads a release asset from the given URL into memory.
fn download_asset(url: &str) -> Result<Vec<u8>, String> {
let response = ureq::get(url)
.set("User-Agent", "dcr-updater")
@@ -155,9 +176,11 @@ fn download_asset(url: &str) -> Result<Vec<u8>, String> {
Ok(data)
}
/// Builds the list of expected asset file names for the given build target.
fn asset_candidates(target: &str) -> Vec<String> {
let mut names = vec![format!("dcr-{target}")];
// Windows releases may publish the binary with an `.exe` suffix.
if target.contains("-windows-") || target.ends_with("-windows") {
names.push(format!("dcr-{target}.exe"));
}
@@ -165,6 +188,7 @@ fn asset_candidates(target: &str) -> Vec<String> {
names
}
/// Creates a unique temporary path next to the current executable for the download.
fn temp_binary_path(current_exe: &Path) -> PathBuf {
let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
@@ -177,6 +201,7 @@ fn temp_binary_path(current_exe: &Path) -> PathBuf {
current_exe.with_extension(extension)
}
/// Sets executable permissions on Unix; no-op on other platforms.
fn set_executable_permissions(_path: &Path) {
#[cfg(unix)]
{
@@ -189,6 +214,7 @@ fn set_executable_permissions(_path: &Path) {
}
}
/// On Linux, returns the pacman package that owns `path`, if any.
#[cfg(target_os = "linux")]
fn pacman_owned_package(path: &Path) -> Option<String> {
let output = Command::new("pacman").arg("-Qoq").arg(path).output().ok()?;
@@ -204,6 +230,7 @@ fn pacman_owned_package(path: &Path) -> Option<String> {
Some(package_name)
}
/// Non-Linux stub: package-manager ownership is not checked.
#[cfg(not(target_os = "linux"))]
fn pacman_owned_package(_path: &Path) -> Option<String> {
None

View File

@@ -15,30 +15,75 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::config::{PROFILE, flags};
use crate::utils::log::warn;
use crate::prelude::*;
use crate::config::{PROFILE, flags};
/// Flags shared by the `build` and `run` CLI commands.
pub struct BuildRunFlags {
/// Build profile name (for example `debug` or `release`).
pub profile: String,
/// Optional target triple or platform name.
pub target: Option<String>,
/// Optional workspace member name (`--workspace <name>`).
pub workspace: Option<String>,
/// Force a rebuild even when artifacts appear up to date.
pub force: bool,
/// Clean build outputs before building.
pub clean: bool,
/// Enable verbose logging during the build or run.
pub verbose: bool,
/// Print the final artifact path to the user.
pub print_artifact_path: bool,
/// Arguments forwarded to the binary after a `--` separator.
pub bin_args: Vec<String>,
}
/// Splits CLI arguments at the first `--` separator.
///
/// Everything before `--` is treated as DCR flags; everything after is
/// forwarded to the built binary as `bin_args`.
///
/// # Parameters
/// - `args`: Full argument list for the command (without the program name).
///
/// # Returns
/// `(dcr_args, bin_args)`. If `--` is absent, `bin_args` is empty.
pub fn split_double_dash(args: &[String]) -> (&[String], &[String]) {
match args.iter().position(|a| a == "--") {
Some(i) => (&args[..i], &args[i + 1..]),
None => (args, &[]),
}
}
/// Parses build/run flags from a list of CLI arguments.
///
/// Recognizes boolean flags (`--force`, `--clean`, `--verbose`,
/// `--print-artifact-path`), value flags (`--workspace`, `--target`), and
/// profile names registered via [`flags`]. Arguments after `--` become
/// [`BuildRunFlags::bin_args`].
///
/// # Parameters
/// - `args`: CLI arguments for build/run (may include `--`).
///
/// # Returns
/// - `Ok(BuildRunFlags)` on success.
/// - `Err(1)` for unknown flags, missing values, or a duplicate profile.
pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
let (dcr_args, bin_args_slice) = split_double_dash(args);
let mut profile = PROFILE.to_string();
let mut target = None;
let mut workspace = None;
let mut force = false;
let mut clean = false;
let mut verbose = false;
let mut iter = args.iter();
let mut print_artifact_path = false;
let mut iter = dcr_args.iter();
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("--");
@@ -54,11 +99,15 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
verbose = true;
continue;
}
if candidate == "print-artifact-path" {
print_artifact_path = true;
continue;
}
if candidate == "workspace" {
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;
@@ -67,20 +116,21 @@ 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;
}
// 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);
}
@@ -91,5 +141,43 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
force,
clean,
verbose,
print_artifact_path,
bin_args: bin_args_slice.to_vec(),
})
}
#[cfg(test)]
mod tests {
use super::*;
/// Converts string literals into owned `String` CLI arguments for tests.
fn s(args: &[&str]) -> Vec<String> {
args.iter().map(|a| a.to_string()).collect()
}
/// Checks that without `--`, the full list stays on the DCR side.
#[test]
fn split_double_dash_empty_tail() {
let args = s(&["--release", "--force"]);
let (left, right) = split_double_dash(&args);
assert_eq!(left, &s(&["--release", "--force"]));
assert!(right.is_empty());
}
/// Checks that arguments after `--` are split into the bin-args slice.
#[test]
fn split_double_dash_with_bin_args() {
let args = s(&["--release", "--", "--test_help", "x"]);
let (left, right) = split_double_dash(&args);
assert_eq!(left, &s(&["--release"]));
assert_eq!(right, &s(&["--test_help", "x"]));
}
/// Checks profile parsing and that bin args after `--` are forwarded.
#[test]
fn parse_run_flags_forwards_after_double_dash() {
let flags = parse_build_run_flags(&s(&["--release", "--", "--test_help"])).unwrap();
assert_eq!(flags.profile, "release");
assert_eq!(flags.bin_args, s(&["--test_help"]));
}
}

View File

@@ -15,22 +15,28 @@
// 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 glob::glob;
use std::process::Command;
/// Formats all C/C++ source files using clang-format.
///
/// Scans the `src/` and `tests/` directories recursively for files with common
/// C/C++ extensions and formats them in place with clang-format.
///
/// 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);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Formats all C/C++ source files using clang-format.");
println!(" Scans src/ and tests/ directories recursively.");
let mut help = crate::utils::help::HelpBuilder::new("dcr fmt");
help.section("USAGE").example("dcr fmt");
help.description("Formats all C/C++ source files using clang-format.")
.note("Scans src/ and tests/ directories recursively.");
help.render();
return 0;
}
// Collect all matching files from src/ and tests/ using glob patterns
let patterns = [
"src/**/*.c",
"src/**/*.cpp",
@@ -62,29 +68,26 @@ pub fn fmt(args: &[String]) -> i32 {
}
if files.is_empty() {
println!(" {} No files to format", colored("Format", BOLD_GREEN));
crate::utils::output::status("Format", "no files to format");
return 0;
}
println!(
" {} {} files",
colored("Formatting", BOLD_GREEN),
files.len()
);
// Format the collected files
crate::utils::output::status("Formatting", format!("{} files", files.len()));
let status = Command::new("clang-format").arg("-i").args(&files).status();
match status {
Ok(s) if s.success() => {
println!(" {} successful", colored("Format", BOLD_GREEN));
crate::utils::output::success("Format successful");
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
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -15,46 +15,47 @@
// 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};
/// Displays the help information for the DCR CLI tool.
///
/// Prints usage, commands, flags, options, examples, and a tip.
///
/// # Returns
/// Always `0`.
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);
println!();
printc("COMMANDS:", BOLD_GREEN);
println!(" new <name> Create a new project");
println!(" init Initialize the current directory as a project");
println!(" build [--profile] Build the project (default: --debug)");
println!(" run [--profile] Build and run the project (default: --debug)");
println!(" add <name> ... Add a dependency to dcr.toml");
println!(" tree Display the dependency tree");
println!(" test Run project tests (alias: tests)");
println!(" fmt Format all C/C++ source files using clang-format");
println!(" lint Run clang-tidy on C/C++ sources");
println!(" setup Show configured package registries");
println!(" clean Remove the target directory");
println!(" gen <subcommand> Generate IDE integration files");
printc("FLAGS:", BOLD_GREEN);
println!(" --help Show command help");
println!(" --update Update dcr to the latest version");
println!(" --version Show dcr version");
println!();
printc("OPTIONS:", BOLD_GREEN);
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);
println!();
printc("TIP:", BOLD_GREEN);
println!(" Run 'dcr <command> --help' for command-specific help.");
use crate::utils::help::HelpBuilder;
let mut help = HelpBuilder::new("DCR (Dexoron Cargo Realization)");
help.description("C/C++ project manager inspired by Cargo.");
help.section("USAGE").example("dcr <command> [options]");
help.section("COMMANDS")
.item("new <name>", "Create a new project")
.item("init", "Initialize the current directory as a project")
.item("build", "Build the project (default: --debug)")
.item("run", "Build and run the project")
.item("add <name> ...", "Add a dependency to dcr.toml")
.item("tree", "Display the dependency tree")
.item("test", "Run project tests (alias: tests)")
.item("fmt", "Format C/C++ sources using clang-format")
.item("lint", "Run clang-tidy on C/C++ sources")
.item("setup", "Show configured package registries")
.item("clean", "Remove build artifacts")
.item("gen <subcommand>", "Generate IDE integration files");
help.section("FLAGS")
.item("--help", "Show command help")
.item("--update", "Update DCR")
.item("--version", "Show DCR version");
help.section("OPTIONS")
.item("--output human|plain|json", "Select output format")
.item("--color auto|always|never", "Select color policy")
.item("--quiet", "Suppress ordinary diagnostics")
.item("--verbose", "Enable detailed diagnostics")
.item("--debug / --release", "Select build profile")
.item("--force", "Force rebuild (build/run)")
.item("--clean", "Clean before build (build/run)")
.item("--all", "Clean all workspace members");
help.example("dcr new hello")
.example("dcr build --release")
.example("dcr run --debug");
help.tip("Run 'dcr <command> --help' for command-specific help.");
help.render();
0
}

View File

@@ -15,27 +15,29 @@
// 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::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 std::{fs, io::Write};
/// Initializes the current working directory as a new DCR project.
///
/// Verifies that the current directory is empty, sets up the project structure by creating
/// `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);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Initializes the current directory as a DCR project.");
println!(" The directory must be empty.");
let mut help = crate::utils::help::HelpBuilder::new("dcr init");
help.section("USAGE").example("dcr init [--vcs <git|none>]");
help.description("Initializes the current directory as a DCR project.")
.note("The directory must be empty.");
help.render();
return 0;
}
// Parse options (--vcs) and collect positional arguments.
let mut vcs_str = None;
let mut clean_args = Vec::new();
let mut iter = args.iter();
@@ -44,7 +46,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=") {
@@ -55,10 +57,11 @@ 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;
}
// Resolve target project name from current directory name and enforce empty directory check.
let items = check_dir(None).unwrap_or_default();
let project_name = std::env::current_dir()
.ok()
@@ -66,72 +69,52 @@ 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,);
return 1;
}
let cwd = std::env::current_dir()
.map(|v| v.to_string_lossy().to_string())
.unwrap_or_else(|_| ".".to_string());
println!("Initializing the project in {cwd}");
crate::utils::output::status("Initializing", format!("project in {cwd}"));
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");
// Create and initialize default manifest (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)
);
crate::utils::output::created("dcr.toml");
// 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)
);
crate::utils::output::created("src/main.c");
// settings VCS
// Resolve target VCS provider or detect existing repositories.
let mut vcs_kind = VcsKind::Git;
if let Some(ref vcs_val) = vcs_str {
match VcsKind::parse(vcs_val) {
Ok(kind) => vcs_kind = kind,
Err(e) => {
error(&e);
error!("{e}");
return 1;
}
}
@@ -145,25 +128,22 @@ 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)
);
crate::utils::output::success("Initialized git repository");
}
} else {
warn(
warn!(
"Git is not installed or not found in PATH. Skipping Git repository initialization.",
);
}
}
println!(
"Project `{}` successfully created\n",
colored(project_name.as_str(), BOLD_GREEN)
);
printc("Next step:", BOLD_GREEN);
printc(" dcr run", BOLD_CYAN);
if let Err(e) = crate::utils::fs::ensure_gitignore_has_dcr(std::path::Path::new(".")) {
warn!("Failed to update .gitignore: {e}");
}
crate::utils::output::success(format!("Initialized project `{project_name}`"));
crate::utils::output::info("Next step:\n dcr run");
0
}

View File

@@ -15,26 +15,27 @@
// 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::fs::find_project_root;
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_YELLOW, colored, printc};
use std::path::{Path, PathBuf};
use std::process::Command;
/// Runs clang-tidy on all C/C++ source files.
///
/// 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);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Runs clang-tidy on all C/C++ source files.");
println!(" Scans source roots (configurable via build.roots) and tests/ directory.");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --fix Apply clang-tidy suggestions automatically");
let mut help = crate::utils::help::HelpBuilder::new("dcr lint");
help.section("USAGE").example("dcr lint [--fix]");
help.description("Runs clang-tidy on all C/C++ source files.")
.note("Scans source roots (configurable via build.roots) and tests/ directory.");
help.section("OPTIONS")
.item("--fix", "Apply clang-tidy suggestions automatically");
help.render();
return 0;
}
@@ -43,7 +44,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;
}
};
@@ -51,11 +52,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;
}
};
@@ -63,7 +64,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;
}
};
@@ -72,6 +73,7 @@ pub fn lint(args: &[String]) -> i32 {
let build_roots = get_list_with_profile(&config, "roots", profile);
let src_disable = get_bool_with_profile(&config, "src_disable", profile, false);
// Construct source roots from config
let mut source_roots: Vec<String> = Vec::new();
for raw in &build_roots {
let p = Path::new(raw);
@@ -94,7 +96,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;
}
};
@@ -112,20 +114,23 @@ pub fn lint(args: &[String]) -> i32 {
files.sort();
if files.is_empty() {
println!(" {} No files to lint", colored("Lint", BOLD_GREEN));
crate::utils::output::status("Lint", "no files to lint");
return 0;
}
println!(
" {} {} files {}",
colored("Linting", BOLD_GREEN),
files.len(),
if do_fix { "(with --fix)" } else { "" }
crate::utils::output::status(
"Linting",
format!(
"{} files {}",
files.len(),
if do_fix { "(with --fix)" } else { "" }
),
);
let mut total_issues = 0u32;
let mut failed = 0u32;
// Execute clang-tidy for each file and count issues
for file in &files {
let mut cmd = Command::new("clang-tidy");
if do_fix {
@@ -137,7 +142,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;
}
};
@@ -155,14 +160,12 @@ pub fn lint(args: &[String]) -> i32 {
if issue_count > 0 {
total_issues += issue_count as u32;
failed += 1;
print!(" {} ", colored("×", BOLD_YELLOW));
println!("{} ({} issues)", file, issue_count);
crate::utils::output::warning(format!("{file} ({issue_count} issues)"));
for issue in &issues {
println!(" {}", issue);
crate::utils::output::compiler_output("stderr", issue);
}
} else {
print!(" {} ", colored("✓", BOLD_GREEN));
println!("{}", file);
crate::utils::output::status("Checked", file);
}
}
@@ -170,19 +173,11 @@ pub fn lint(args: &[String]) -> i32 {
let passed = total - failed as usize;
if failed == 0 {
println!(
" {} {} files, no issues",
colored("Lint", BOLD_GREEN),
total
);
crate::utils::output::success(format!("Finished checking {total} files; no issues"));
} else {
println!(
" {} {}/{} files passed, {} issues found",
colored("Lint", BOLD_YELLOW),
passed,
total,
total_issues
);
crate::utils::output::warning(format!(
"Finished checking {total} files; {passed} passed, {total_issues} issues found"
));
}
0

View File

@@ -15,6 +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/>.
/// DCR CLI module.
///
/// This module declares submodules for all DCR commands.
pub mod add;
pub mod build;
pub mod clean;

View File

@@ -15,29 +15,32 @@
// 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::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 std::{fs, io::Write};
/// Executes the `dcr new` command to scaffold a new C/C++ project directory structure.
///
/// Handles command-line argument parsing, directory creation, default configuration generation (`dcr.toml`),
/// 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);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Creates a new C/C++ project with the given name.");
println!(" The name may only contain ASCII letters, digits, '_' and '-'.");
let mut help = crate::utils::help::HelpBuilder::new("dcr new");
help.section("USAGE")
.example("dcr new <name> [--vcs <git|none>]");
help.description("Creates a new C/C++ project with the given name.")
.note("The name may only contain ASCII letters, digits, '_' and '-'.");
help.render();
return 0;
}
let items = check_dir(None).unwrap_or_default();
// Parse CLI options (--vcs) while extracting positional arguments for the project name.
let mut vcs_str = None;
let mut clean_args = Vec::new();
let mut iter = args.iter();
@@ -46,7 +49,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=") {
@@ -57,102 +60,72 @@ 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)
);
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,);
return 1;
}
if items.contains(project_name) {
error(&format!(
"Directory `{}` already exists\n",
colored(project_name, BOLD_CYAN)
));
printc("Hint:", BOLD_CYAN);
println!(
error!("Directory `{}` already exists", project_name);
crate::utils::output::info(format!(
" Use `{}` to initialize an existing project\n or specify a different project name",
colored("dcr init", BOLD_CYAN)
);
"dcr init"
));
return 1;
}
crate::utils::output::status("Creating", format!("project `{project_name}`"));
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),
project_name
);
crate::utils::output::created(format!("directory `{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)
);
crate::utils::output::created("dcr.toml");
// 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)
);
crate::utils::output::created("src/main.c");
// settings VCS
// Resolve target VCS mode based on CLI flags or parent directory state.
let mut vcs_kind = VcsKind::Git;
if let Some(ref vcs_val) = vcs_str {
match VcsKind::parse(vcs_val) {
Ok(kind) => vcs_kind = kind,
Err(e) => {
error(&e);
error!("{e}");
return 1;
}
}
@@ -166,25 +139,23 @@ 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)
);
crate::utils::output::success("Initialized git repository");
}
} else {
warn(
warn!(
"Git is not installed or not found in PATH. Skipping Git repository initialization.",
);
}
}
println!(
"Project `{}` successfully created\n",
colored(project_name, BOLD_GREEN)
);
printc("Next step:", BOLD_GREEN);
printc(&format!(" cd {}\n dcr run", project_name), BOLD_CYAN);
let project_path = std::path::Path::new(project_name);
if let Err(e) = crate::utils::fs::ensure_gitignore_has_dcr(project_path) {
warn!("Failed to update .gitignore: {e}");
}
crate::utils::output::success(format!("Created project `{project_name}`"));
crate::utils::output::info(format!("Next steps:\n cd {project_name}\n dcr run"));
0
}

View File

@@ -15,18 +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::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::fs::{find_project_root, with_dir};
use std::{path::Path, process::Command};
/// Retrieves the run command from the config, preferring target-specific,
/// then profile-specific, then the base `run.cmd`.
fn get_run_cmd(
config: &Config,
profile: &str,
@@ -55,42 +55,47 @@ fn get_run_cmd(
}
}
/// Handles the `dcr run` subcommand: parses flags, finds the project root,
/// builds if needed, and runs the resulting binary or configured command.
///
/// # Parameters
/// - `args`: Tokens after `dcr run` (build flags; after `--` → binary args).
///
/// # Returns
/// 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]",
BOLD_CYAN,
);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Builds and runs the project. Only available for kind = \"bin\".");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --debug Run with debug profile (default)");
println!(" --release Run with release profile");
println!(" --target <triple> Cross-compile for the given target");
println!(" --force Force a full rebuild");
println!(" --clean Clean before building");
println!(" --verbose Print detailed build output");
let mut help = crate::utils::help::HelpBuilder::new("dcr run");
help.section("USAGE").example("dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [-- <args>...]");
help.description("Builds and runs the project. Only available for kind = \"bin\".")
.note("Arguments after `--` are passed to the built binary (cargo-style).");
help.section("OPTIONS")
.item("--debug", "Run with debug profile (default)")
.item("--release", "Run with release profile")
.item("--target <triple>", "Cross-compile for the given target")
.item("--force", "Force a full rebuild")
.item("--clean", "Clean before building")
.item("--verbose", "Print detailed build output")
.item("-- <args>...", "Arguments forwarded to the binary");
help.render();
return 0;
}
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;
}
};
@@ -99,7 +104,8 @@ pub fn run(args: &[String]) -> i32 {
}) {
Ok(cfg) => cfg,
Err(err) => {
error(&err);
error!("{err}");
return 1;
}
};
@@ -112,12 +118,9 @@ pub fn run(args: &[String]) -> i32 {
if let Some(cmd) = get_run_cmd(&config, &flags.profile, flags.target.as_deref(), "") {
let build_status = build(&args_for_build(&flags));
if build_status == 0 {
println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
cmd
);
return run_shell(&cmd);
let display = display_run_cmd(&cmd, &flags.bin_args);
crate::utils::output::status("run", display);
return run_shell_with_args(&cmd, &flags.bin_args);
}
return build_status;
}
@@ -130,11 +133,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;
}
};
@@ -146,9 +150,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;
}
@@ -159,7 +163,8 @@ pub fn run(args: &[String]) -> i32 {
}) {
Ok(code) => code,
Err(e) => {
error(&e);
error!("{e}");
1
}
};
@@ -168,12 +173,13 @@ pub fn run(args: &[String]) -> i32 {
match run_project(&root, &flags, None) {
Ok(code) => code,
Err(e) => {
error(&e);
error!("{e}");
1
}
}
}
/// Builds and runs a single project at `root` (optionally as a workspace member).
fn run_project(
root: &Path,
flags: &crate::cli::flags::BuildRunFlags,
@@ -204,19 +210,17 @@ fn run_project(
let out_dir =
crate::cli::build::get_build_string_with_profile(&config, "out_dir", &flags.profile);
let normalized_target_dir = if !out_dir.is_empty() {
Some(out_dir)
} else {
match workspace_root {
Some(wr) => target
.as_ref()
.and_then(|t| crate::utils::build::normalize_target(t, &flags.profile))
.map(|rel| wr.join(&rel).to_string_lossy().to_string()),
None => target
.as_ref()
.and_then(|t| crate::utils::build::normalize_target(t, &flags.profile)),
}
};
let build_target_str = target.clone().unwrap_or_default();
let has_explicit = target.as_ref().is_some_and(|t| !t.trim().is_empty());
let normalized_target_dir = Some(crate::utils::build::resolve_artifact_target_dir(
root,
workspace_root,
&flags.profile,
&build_target_str,
&out_dir,
has_explicit,
));
let version = config
.get("package.version")
@@ -250,32 +254,28 @@ fn run_project(
);
if build_status == 0 {
if let Some(cmd) = run_cmd {
println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
cmd
);
return Ok(run_shell(&cmd));
let display = display_run_cmd(&cmd, &flags.bin_args);
crate::utils::output::status("run", display);
return Ok(run_shell_with_args(&cmd, &flags.bin_args));
}
println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
bin_path
);
let display = display_bin_run(&bin_path, &flags.bin_args);
crate::utils::output::status("run", display);
return Ok(run_binary(
project_name,
&flags.profile,
normalized_target_dir.as_deref(),
&flags.bin_args,
));
}
let fallback_code = if let Some(cmd) = run_cmd {
run_shell(&cmd)
run_shell_with_args(&cmd, &flags.bin_args)
} else {
run_binary(
project_name,
&flags.profile,
normalized_target_dir.as_deref(),
&flags.bin_args,
)
};
if fallback_code != 1 {
@@ -285,6 +285,7 @@ fn run_project(
Err("Fix errors in the code to run the project".to_string())
}
/// Builds the list of arguments for the build command based on the provided flags.
fn args_for_build(flags: &crate::cli::flags::BuildRunFlags) -> Vec<String> {
let mut args = Vec::new();
args.push(format!("--{}", flags.profile));
@@ -308,6 +309,7 @@ fn args_for_build(flags: &crate::cli::flags::BuildRunFlags) -> Vec<String> {
args
}
/// Constructs build arguments specifically for a workspace member.
fn args_for_workspace_build(
flags: &crate::cli::flags::BuildRunFlags,
member_name: &str,
@@ -332,6 +334,7 @@ fn args_for_workspace_build(
args
}
/// Executes the given shell command and returns its exit code.
fn run_shell(cmd: &str) -> i32 {
let status = if cfg!(target_os = "windows") {
Command::new("cmd").arg("/C").arg(cmd).status()
@@ -345,6 +348,67 @@ fn run_shell(cmd: &str) -> i32 {
}
}
/// Runs the command, appending escaped binary arguments if provided.
fn run_shell_with_args(cmd: &str, bin_args: &[String]) -> i32 {
if bin_args.is_empty() {
return run_shell(cmd);
}
let mut full = cmd.to_string();
for arg in bin_args {
full.push(' ');
full.push_str(&shell_escape(arg));
}
run_shell(&full)
}
/// Formats the run command string for console output, appending arguments if any.
fn display_run_cmd(cmd: &str, bin_args: &[String]) -> String {
if bin_args.is_empty() {
return cmd.to_string();
}
let mut display = cmd.to_string();
for arg in bin_args {
display.push(' ');
display.push_str(arg);
}
display
}
/// Formats the binary path for console display, appending arguments if any.
fn display_bin_run(bin_path: &str, bin_args: &[String]) -> String {
if bin_args.is_empty() {
return bin_path.to_string();
}
let mut display = bin_path.to_string();
for arg in bin_args {
display.push(' ');
display.push_str(arg);
}
display
}
/// Escapes the argument for safe shell execution, using double quotes on Windows and single quotes on Unix.
fn shell_escape(arg: &str) -> String {
if cfg!(target_os = "windows") {
// cmd.exe: wrap in double quotes and escape embedded quotes.
let escaped = arg.replace('"', "\\\"");
format!("\"{escaped}\"")
} else {
// POSIX sh: single-quote and escape embedded single quotes as '\''.
let mut out = String::from("'");
for ch in arg.chars() {
if ch == '\'' {
out.push_str("'\\''");
} else {
out.push(ch);
}
}
out.push('\'');
out
}
}
/// Replaces version info and profile in the command template.
fn substitute_run_vars(cmd: &str, profile: &str, version: &str) -> String {
let info = parse_version_info(version);
substitute_vars(cmd, &info, profile, "")

View File

@@ -15,30 +15,31 @@
// 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};
/// 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);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Sets up DCR registries. Downloads and indexes");
println!(" package registries for dependency resolution.");
let mut help = crate::utils::help::HelpBuilder::new("dcr setup");
help.section("USAGE").example("dcr setup");
help.description("Show configured package registries and their priorities.");
help.render();
return 0;
}
println!("Setting up DCR registries...");
// Load and process the registries, returning 0 on success or 1 on error
match crate::core::registry::RegistryManager::load() {
Ok(manager) => {
println!("Loaded {} registries.", manager.config.registry.len());
let mut lines = vec![format!(
"Loaded {} registries (configured)",
manager.config.registry.len()
)];
for (name, reg) in manager.config.registry {
println!("- {}: {} (priority {})", name, reg.url, reg.priority);
lines.push(format!(" {name}: {} (priority {})", reg.url, reg.priority));
}
crate::say!("{}", lines.join("\n"));
0
}
Err(e) => {
println!("Error: {}", e);
crate::utils::output::error!("{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::cli::build;
use crate::config::{FILE_DCR_TEST_H, FILE_TEST_C, flags};
use crate::core::build_config::Config;
@@ -23,32 +25,29 @@ use crate::utils::build::{
get_string_with_profile, resolve_compiler, resolve_pkg_config_flags,
};
use crate::utils::fs::{find_project_root, with_dir};
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_RED, RESET, colored, printc};
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);
println!();
printc("ALIASES:", BOLD_GREEN);
println!(" dcr tests");
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Runs project tests and prints a unified testsuite report.");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --init Create tests/dcr_test.h in current project");
println!(" --debug Build and run tests with debug profile (default)");
println!(" --release Build and run tests with release profile");
let mut help = crate::utils::help::HelpBuilder::new("dcr test");
help.section("USAGE")
.example("dcr test [--init] [--debug | --release]");
help.section("ALIASES").example("dcr tests");
help.description("Runs project tests and prints a unified testsuite report.");
help.section("OPTIONS")
.item("--init", "Create tests/dcr_test.h in current project")
.item(
"--debug",
"Build and run tests with debug profile (default)",
)
.item("--release", "Build and run tests with release profile");
help.render();
return 0;
}
if arg == "--init" {
@@ -59,25 +58,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;
}
};
@@ -86,7 +85,8 @@ pub fn test(args: &[String]) -> i32 {
match with_dir(&root, ensure_test_header) {
Ok(()) => {}
Err(msg) => {
error(&msg);
error!("{msg}");
return 1;
}
}
@@ -95,12 +95,13 @@ pub fn test(args: &[String]) -> i32 {
match with_dir(&root, || run_testsuite(profile)) {
Ok(code) => code,
Err(msg) => {
error(&msg);
error!("{msg}");
1
}
}
}
/// Executes the testsuite for the given build profile.
fn run_testsuite(profile: &str) -> Result<i32, String> {
if build::build(&[format!("--{profile}")]) != 0 {
return Ok(1);
@@ -132,8 +133,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());
@@ -172,7 +177,7 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
stdout.push_str(&String::from_utf8_lossy(&out.stdout));
let stderr = String::from_utf8_lossy(&out.stderr);
if !stderr.trim().is_empty() {
eprint!("{}", stderr);
crate::utils::output::compiler_output("stderr", &stderr);
}
if !out.status.success() {
suite_success = false;
@@ -187,19 +192,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);
crate::utils::output::success(format!("Passed {name}"));
pass += 1;
parsed_any = true;
continue;
}
if let Some(name) = line.strip_prefix("[SKIP] ") {
println!("{} {}", colored("[SKIP]", BOLD_BLUE), name);
crate::utils::output::status("Skipped", name);
skip += 1;
parsed_any = true;
continue;
}
if let Some(name) = line.strip_prefix("[FAIL] ") {
println!("{} {}", colored("[FAIL]", BOLD_RED), name);
crate::utils::output::error(format!("Failed {name}"));
fail += 1;
parsed_any = true;
continue;
@@ -207,17 +212,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);
crate::utils::output::success(format!("Passed {name}"));
pass += 1;
parsed_any = true;
}
"SKIP" => {
println!("{} {}", colored("[SKIP]", BOLD_BLUE), name);
crate::utils::output::status("Skipped", name);
skip += 1;
parsed_any = true;
}
"FAIL" => {
println!("{} {}", colored("[FAIL]", BOLD_RED), name);
crate::utils::output::error(format!("Failed {name}"));
fail += 1;
parsed_any = true;
}
@@ -229,11 +234,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);
crate::utils::output::success(format!("Passed {name}"));
}
pass = declared.len() as i32;
} else {
println!("{} testsuite", colored("[FAIL]", BOLD_RED));
crate::utils::output::error("testsuite failed");
fail = 1;
}
}
@@ -244,15 +249,10 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
declared.len() as i32
};
println!();
println!("{}", colored("=====================", BOLD_GREEN));
println!("{}", colored(" Testsuite summary ", BOLD_GREEN));
println!("{}", colored("=====================", 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));
crate::utils::output::status(
"test result:",
format!("{pass} passed; {fail} failed; {skip} skipped; {total} total"),
);
if fail > 0 || !suite_success {
return Ok(1);
@@ -260,6 +260,7 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
Ok(0)
}
/// Collects all .c source files from the tests/ directory.
fn collect_test_sources() -> Result<Vec<PathBuf>, String> {
let tests_dir = Path::new("./tests");
if !tests_dir.is_dir() {
@@ -278,6 +279,7 @@ fn collect_test_sources() -> Result<Vec<PathBuf>, String> {
Ok(sources)
}
/// Returns a list of include directories for test compilation.
fn test_include_dirs(config: &Config, profile: &str) -> Vec<String> {
let mut dirs = vec![
"./tests".to_string(),
@@ -288,6 +290,7 @@ fn test_include_dirs(config: &Config, profile: &str) -> Vec<String> {
dirs
}
/// Compiles a single test source file to an object file.
fn compile_test_source(
compiler: &str,
standard: &str,
@@ -317,6 +320,7 @@ fn compile_test_source(
Ok(())
}
/// Links the test object file into an executable binary.
fn link_test_binary(
compiler: &str,
ldflags: &[String],
@@ -343,26 +347,29 @@ fn link_test_binary(
Ok(())
}
/// Prints the stdout and stderr from a command execution.
fn print_command_output(output: &std::process::Output) {
if !output.stdout.is_empty() {
print!("{}", String::from_utf8_lossy(&output.stdout));
crate::utils::output::write_stdout(&format!("{}", String::from_utf8_lossy(&output.stdout)));
}
if !output.stderr.is_empty() {
eprint!("{}", String::from_utf8_lossy(&output.stderr));
crate::utils::output::compiler_output("stderr", &String::from_utf8_lossy(&output.stderr));
}
}
/// Creates the default test files (dcr_test.h and test.c) in the tests/ directory.
fn ensure_test_header() -> Result<(), String> {
fs::create_dir_all("./tests").map_err(|_| "Failed to create tests/".to_string())?;
fs::write("./tests/dcr_test.h", FILE_DCR_TEST_H)
.map_err(|_| "Failed to write tests/dcr_test.h".to_string())?;
println!("Created tests/dcr_test.h");
crate::utils::output::created("tests/dcr_test.h");
fs::write("./tests/test.c", FILE_TEST_C)
.map_err(|_| "Failed to write tests/test.c".to_string())?;
println!("Created tests/test.c");
crate::utils::output::created("tests/test.c");
Ok(())
}
/// Extracts test case names from a C source file by parsing TEST_CASE macros.
fn extract_test_names_path(path: &Path) -> Vec<String> {
let Ok(content) = fs::read_to_string(path) else {
return Vec::new();
@@ -378,11 +385,3 @@ fn extract_test_names_path(path: &Path) -> Vec<String> {
}
names
}
fn print_field(label: &str, value: i32, color: &str) {
if value == 0 {
println!("{}: {}", label, value);
} else {
println!("{}{}: {}{}", color, label, value, RESET);
}
}

View File

@@ -15,20 +15,23 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::prelude::*;
use crate::core::build_config::Config;
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use toml::Value;
/// Displays the dependency tree of the current project.
///
/// This function checks for the --help flag and prints usage information if present.
/// 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);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Displays the dependency tree of the current project.");
let mut help = crate::utils::help::HelpBuilder::new("dcr tree");
help.section("USAGE").example("dcr tree");
help.description("Displays the dependency tree of the current project.");
help.render();
return 0;
}
@@ -36,7 +39,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;
}
};
@@ -50,7 +53,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);
crate::utils::output::write_stdout_raw(&format!("{} v{}\n", name, version));
let mut seen = HashSet::new();
seen.insert(name.to_string());
@@ -62,6 +65,9 @@ pub fn tree(args: &[String]) -> i32 {
0
}
/// Recursively prints the dependency tree starting from the given dependencies table.
///
/// Sorts dependencies for consistent ordering and handles path resolution for sub-dependencies.
fn print_deps(
deps: &toml::value::Table,
base_path: &Path,
@@ -81,6 +87,7 @@ fn print_deps(
let mut sub_deps = None;
let mut resolved_path = None;
// Resolve dependency path if it's a path-based dependency
if let Some(path) = &dep_path {
let dcr_toml = path.join("dcr.toml");
if dcr_toml.exists()
@@ -107,6 +114,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()
}
@@ -115,8 +135,12 @@ fn print_deps(
};
}
println!("{}{}{}{}", prefix, connector, name, version);
crate::utils::output::write_stdout_raw(&format!(
"{}{}{}{}\n",
prefix, connector, name, version
));
// Recurse only if sub-dependencies exist and this dep hasn't been seen
if let Some(s_deps) = sub_deps
&& !seen.contains(*name)
{
@@ -130,6 +154,7 @@ fn print_deps(
}
}
/// Resolves the filesystem path for a dependency entry if it specifies a "path:" prefix or table with "path" key.
fn resolve_dep_path(value: &Value, base_path: &Path) -> Option<PathBuf> {
match value {
Value::String(s) => s

View File

@@ -96,6 +96,7 @@ int main(int argc, char **argv) {
}
"#;
/// Returns build flags for the given profile as a static slice, or None if empty.
pub fn flags(profile: &str) -> Option<&'static [&'static str]> {
let f = crate::utils::build::default_profile_flags(profile);
if f.is_empty() { None } else { Some(f) }

View File

@@ -22,6 +22,7 @@ use std::fs::{self, File, OpenOptions};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::Path;
/// File wrapper that shifts all seek offsets (used to skip the boot sector on a FAT image).
struct OffsetFile<T> {
inner: T,
offset: u64,
@@ -73,6 +74,7 @@ impl<T: Seek> Seek for OffsetFile<T> {
}
}
/// Parses a size string like `1.5m` or `500k` into bytes (1024-based; empty → 0).
fn parse_size(s: &str) -> Result<u64, String> {
let s = s.trim();
if s.is_empty() {
@@ -101,6 +103,7 @@ fn parse_size(s: &str) -> Result<u64, String> {
Ok((val * multiplier) as u64)
}
/// Creates a FAT image from layout config, substituting `{profile}` and optional boot sector.
pub fn pack_archive(
project_root: &Path,
config: &ArchiveConfig,
@@ -142,6 +145,7 @@ pub fn pack_archive(
if offset == 0
&& let Some(boot_rel) = &config.bootsector
{
// Load and write bootsector if present
let boot_rel_substituted = boot_rel.replace("{profile}", profile);
let boot_path = project_root.join(&boot_rel_substituted);
if boot_path.is_file() {

View File

@@ -15,14 +15,14 @@
// 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};
use std::process::Command;
use std::time::Instant;
/// Returns the artifact directory path based on the build context.
pub(crate) fn artifact_dir(ctx: &BuildContext) -> PathBuf {
match ctx.target_dir {
Some(dir) => Path::new(dir).to_path_buf(),
@@ -30,6 +30,77 @@ pub(crate) fn artifact_dir(ctx: &BuildContext) -> PathBuf {
}
}
/// Resolves the artifact path for a given kind/profile/name.
///
/// # Parameters
/// - `kind`: Artifact kind (`bin`, `staticlib`, `sharedlib`, `flat-bin`, `efi`, `elf`, …).
/// - `profile`: Build profile (`debug` / `release` / custom).
/// - `project_name`: Default basename when `output_filename` is unset.
/// - `target_dir`: Optional artifact directory override.
/// - `output_filename` / `output_extension`: Optional name/ext overrides.
///
/// # Returns
/// Relative/absolute path string, or `None` for `none` / `custom` kinds.
pub fn resolve_artifact_path(
kind: &str,
profile: &str,
project_name: &str,
target_dir: Option<&str>,
output_filename: Option<&str>,
output_extension: Option<&str>,
) -> Option<String> {
if crate::utils::build::is_flat_bin(kind) {
let name = output_filename.unwrap_or(project_name);
let ext = output_extension.unwrap_or("bin");
let file = if ext.is_empty() {
name.to_string()
} else {
format!("{name}.{ext}")
};
let dir = match target_dir {
Some(dir) => Path::new(dir).to_path_buf(),
None => Path::new("./target").join(profile),
};
return Some(dir.join(file).to_string_lossy().to_string());
}
if matches!(kind, "none" | "custom") {
return None;
}
let name = output_filename.unwrap_or(project_name);
let ext = output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
format!("{}.{}", name, ext)
};
let path = match kind {
"staticlib" => platform::lib_path(profile, &final_name, target_dir),
"sharedlib" => platform::shared_lib_path(profile, &final_name, target_dir),
"efi" => platform::efi_path(profile, &final_name, target_dir),
"elf" => platform::elf_path(profile, &final_name, target_dir),
_ => platform::bin_path(profile, &final_name, target_dir),
};
Some(path)
}
/// Converts a relative or absolute path to an absolute path by joining with project root if needed.
pub fn absolute_artifact_path(project_root: &Path, relative_or_abs: &str) -> PathBuf {
let p = Path::new(relative_or_abs);
let joined = if p.is_absolute() {
p.to_path_buf()
} else {
let stripped = relative_or_abs
.strip_prefix("./")
.unwrap_or(relative_or_abs);
project_root.join(stripped)
};
crate::utils::fs::canonicalize_path(&joined)
}
/// Returns the output path for a flat binary artifact.
pub(crate) fn flat_output_path(ctx: &BuildContext) -> String {
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("bin");
@@ -41,6 +112,7 @@ pub(crate) fn flat_output_path(ctx: &BuildContext) -> String {
artifact_dir(ctx).join(file).to_string_lossy().to_string()
}
/// Returns the output path for a flat binary artifact from a source file.
pub(crate) fn flat_source_output_path(ctx: &BuildContext, source: &str) -> String {
let stem = Path::new(source)
.file_stem()
@@ -55,6 +127,7 @@ pub(crate) fn flat_source_output_path(ctx: &BuildContext, source: &str) -> Strin
artifact_dir(ctx).join(file).to_string_lossy().to_string()
}
// Helper to find a suitable objcopy tool in PATH
fn resolve_objcopy() -> Result<String, String> {
for candidate in ["llvm-objcopy", "objcopy", "gobjcopy"] {
if Command::new(candidate)
@@ -73,6 +146,7 @@ fn resolve_objcopy() -> Result<String, String> {
)
}
/// Converts an object/ELF to a raw binary via `objcopy -O binary` (skips if up to date).
pub(crate) fn objcopy_binary(ctx: &BuildContext, input: &str, output: &str) -> Result<(), String> {
if let Some(parent) = Path::new(output).parent() {
std::fs::create_dir_all(parent).map_err(|e| format!("output dir error: {e}"))?;
@@ -89,6 +163,7 @@ pub(crate) fn objcopy_binary(ctx: &BuildContext, input: &str, output: &str) -> R
common::run_command_sync_output(&mut cmd)
}
/// Links objects into an intermediate ELF, then converts it to a flat binary via objcopy.
pub(crate) fn link_flat_binary(
ctx: &BuildContext,
objects: &[String],
@@ -150,6 +225,7 @@ pub(crate) fn link_flat_binary(
Ok(common::elapsed_secs(start_time))
}
/// Archives objects into a static library.
pub(crate) fn archive_static(
ctx: &BuildContext,
objects: &[String],
@@ -160,14 +236,12 @@ pub(crate) fn archive_static(
return Ok(common::elapsed_secs(start_time));
}
let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") {
"lib"
} else {
"ar"
});
let archiver = ctx
.archiver
.unwrap_or_else(|| default_archiver(ctx.compiler));
let mut cmd = Command::new(archiver);
if cfg!(target_os = "windows") && archiver == "lib" {
if cfg!(target_os = "windows") && is_msvc_lib(archiver) {
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
} else {
cmd.arg("rcs").arg(&lib_path);
@@ -178,6 +252,7 @@ pub(crate) fn archive_static(
run(ctx, cmd, start_time)
}
/// Links objects into a binary or library based on artifact kind.
pub(crate) fn link_binary(
ctx: &BuildContext,
objects: &[String],
@@ -243,6 +318,29 @@ pub(crate) fn link_binary(
run(ctx, cmd, start_time)
}
/// Returns the default archiver name based on compiler and platform.
fn default_archiver(compiler: &str) -> &'static str {
let c = compiler.to_lowercase();
if cfg!(target_os = "windows") {
if c.contains("cl") && !c.contains("clang") {
"lib"
} else if c.contains("clang") {
"llvm-ar"
} else {
"ar"
}
} else {
"ar"
}
}
/// Checks if the archiver is an MSVC lib tool.
fn is_msvc_lib(archiver: &str) -> bool {
let a = archiver.to_lowercase();
a == "lib" || a == "lib.exe" || a.ends_with("\\lib.exe") || a.ends_with("/lib.exe")
}
/// Runs a command and handles success/failure with timing.
fn run(ctx: &BuildContext, mut cmd: Command, start_time: Instant) -> Result<f64, String> {
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
@@ -264,3 +362,64 @@ fn run(ctx: &BuildContext, mut cmd: Command, start_time: Instant) -> Result<f64,
Err(format!("Build failed:\nstdout: {stdout}\nstderr: {stderr}"))
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Default `bin` kind resolves via platform bin path.
#[test]
fn resolve_bin_default() {
let p = resolve_artifact_path("bin", "debug", "hello", None, None, None).unwrap();
assert_eq!(p, platform::bin_path("debug", "hello", None));
}
/// Custom `target_dir` places the binary under that directory.
#[test]
fn resolve_bin_custom_target_dir() {
let p = resolve_artifact_path("bin", "debug", "hello", Some("/out"), None, None).unwrap();
let name = if cfg!(windows) { "hello.exe" } else { "hello" };
let expected = Path::new("/out").join(name);
assert_eq!(p, expected.to_string_lossy());
}
/// `staticlib` kind resolves to a platform static library path.
#[test]
fn resolve_staticlib() {
let p = resolve_artifact_path("staticlib", "release", "mylib", Some("dist"), None, None)
.unwrap();
let name = if cfg!(windows) {
"mylib.lib"
} else {
"libmylib.a"
};
let expected = Path::new("dist").join(name);
assert_eq!(p, expected.to_string_lossy());
}
/// Custom output filename and extension override the default name.
#[test]
fn resolve_with_filename_ext() {
let p = resolve_artifact_path(
"bin",
"debug",
"pkg",
Some("/out"),
Some("KERNEL"),
Some("ELF"),
)
.unwrap();
assert_eq!(
p,
std::path::Path::new("/out")
.join("KERNEL.ELF")
.to_string_lossy()
);
}
/// `none` kind has no artifact path.
#[test]
fn resolve_none_kind() {
assert!(resolve_artifact_path("none", "debug", "x", None, None, None).is_none());
}
}

View File

@@ -15,21 +15,23 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::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};
use std::process::Command;
use std::time::Instant;
/// Compiles C/C++ sources and links/archives by kind (`bin`, libs, `flat-bin`, or compile-only).
///
/// Handles Qt preprocessing when enabled. Returns elapsed seconds.
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let compiler = if ctx.compiler.is_empty() {
"cc"
} else {
ctx.compiler
};
// Reject unsupported flat-bin with Qt
if is_flat_bin(ctx.kind) && ctx.qt {
return Err("flat-bin is not supported with build.qt = true".to_string());
}
@@ -69,6 +71,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
artifact::link_binary(ctx, &objects, compiler, start_time)
}
/// Collects source files from the provided source roots for the build context.
///
/// Delegates to common source collection after resolving language-specific extensions.
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
let extensions = source_extensions(ctx.language);
common::collect_sources(
@@ -79,14 +84,19 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
)
}
/// Source file extensions for `language` (owned `Vec` of static slices).
fn source_extensions(language: &str) -> Vec<&'static str> {
crate::core::build::common::source_extensions(language)
}
/// Retrieves default compiler flags for the specified build profile.
fn default_flags(profile: &str) -> &'static [&'static str] {
crate::utils::build::default_profile_flags(profile)
}
/// Compiles all provided sources into object files in parallel.
///
/// Prepares object file paths and spawns parallel tasks for each source compilation.
fn build_objects(
compiler: &str,
sources: &[String],
@@ -100,6 +110,7 @@ fn build_objects(
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
// Compile objects in parallel
common::parallel_build(
sources.len(),
|i| {
@@ -117,6 +128,9 @@ fn build_objects(
Ok(objects)
}
/// Compiles a single source file to an object file.
///
/// Creates object directory if needed, checks for rebuild necessity, constructs and runs the compiler command with appropriate flags.
fn build_object(
compiler: &str,
source: &str,
@@ -128,6 +142,7 @@ fn build_object(
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
// Skip if object is already up to date
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
@@ -183,6 +198,8 @@ fn build_object(
let use_dcr_defaults = ctx.cflags.is_empty()
&& !(ctx.freestanding || is_bare_metal_target(ctx.target) || is_flat_bin(ctx.kind));
// Use default flags only if no custom flags and not special target
if use_dcr_defaults {
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
@@ -211,6 +228,7 @@ fn build_object(
common::run_command_sync_output(&mut cmd)
}
/// Returns the assembly language flag for the compiler based on source extension.
fn asm_lang_flag(source: &str) -> Option<&'static str> {
crate::core::build::common::asm_lang_flag(source)
}

View File

@@ -22,6 +22,7 @@ pub mod msvc;
use crate::core::build::language::asm::{fasm, gas, masm, nasm};
use crate::core::build::language::llvm_ir;
/// Configuration context for the build process, including compiler, target, and output settings.
pub struct BuildContext<'a> {
pub profile: &'a str,
pub project_name: &'a str,
@@ -56,6 +57,7 @@ pub struct BuildContext<'a> {
pub qt: bool,
}
/// Trait defining the build interface for different compilers and languages.
pub trait Builder {
#[allow(dead_code)]
fn id(&self) -> &'static str;
@@ -63,12 +65,19 @@ pub trait Builder {
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String>;
}
/// Concrete builder for C/C++ languages using gcc/clang.
struct Cc;
/// Concrete builder for MSVC and clang-cl.
struct Msvc;
/// Concrete builder for NASM assembly.
struct Nasm;
/// Concrete builder for GNU Assembler (gas).
struct Gas;
/// Concrete builder for MASM assembler.
struct Masm;
/// Concrete builder for FASM assembler.
struct Fasm;
/// Concrete builder for LLVM IR.
struct Llc;
impl Builder for Cc {
@@ -156,6 +165,7 @@ impl Builder for Llc {
}
pub fn builder_for(compiler: &str) -> &'static dyn Builder {
// Normalize compiler name to lowercase for matching.
let c = compiler.to_lowercase();
if c.contains("clang-cl") {
return &Msvc;
@@ -181,6 +191,7 @@ pub fn builder_for(compiler: &str) -> &'static dyn Builder {
&Cc
}
/// Main build entrypoint that validates the compiler and delegates to the selected builder.
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if !check_compiler_exists(ctx.compiler) {
return Err(format!(
@@ -188,9 +199,11 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
ctx.compiler
));
}
// Delegate to the concrete builder's build method.
builder_for(ctx.compiler).build(ctx)
}
/// True if `compiler --version` can be spawned (exit status is ignored).
fn check_compiler_exists(compiler: &str) -> bool {
let name = if compiler.is_empty() {
"cc"
@@ -209,10 +222,12 @@ fn check_compiler_exists(compiler: &str) -> bool {
.is_ok()
}
/// Collects source code files by delegating to the builder selected for the compiler.
pub fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
builder_for(ctx.compiler).collect_sources(ctx)
}
/// Unit test verifying that builder_for correctly identifies the builder for various compiler names.
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -15,16 +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::builder::BuildContext;
use crate::core::build::builder::artifact;
use crate::core::build::common;
/// MSVC build backend for DCR.
///
/// 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, 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> {
let compiler = if ctx.compiler.is_empty() {
"cl"
@@ -60,8 +61,13 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let elapsed = common::elapsed_secs(start_time);
return Ok(elapsed);
}
let mut cmd = Command::new(ctx.archiver.unwrap_or("lib"));
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
let archiver = ctx.archiver.unwrap_or("lib");
let mut cmd = Command::new(archiver);
if archiver == "lib" || archiver.eq_ignore_ascii_case("lib.exe") {
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
} else {
cmd.arg("rcs").arg(&lib_path);
}
for obj in &objects {
cmd.arg(obj);
}
@@ -154,6 +160,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}
}
/// Links the flat binary output for MSVC build targets.
fn link_flat_msvc(
ctx: &BuildContext,
compiler: &str,
@@ -209,6 +216,7 @@ fn link_flat_msvc(
Ok(common::elapsed_secs(start_time))
}
/// Collects all source files for the build based on language extensions.
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
let extensions = source_extensions(ctx.language);
common::collect_sources(
@@ -219,10 +227,12 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
)
}
/// Source file extensions for `language` (owned `Vec` of static slices).
fn source_extensions(language: &str) -> Vec<&'static str> {
crate::core::build::common::source_extensions(language)
}
/// Maps the language and standard version to the corresponding MSVC flag string.
fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> {
let lang = language.to_lowercase();
let std = standard.to_lowercase();
@@ -246,6 +256,7 @@ fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String>
Err("Unsupported language".to_string())
}
/// Returns the MSVC architecture flag based on the target platform.
fn msvc_arch_flag(platform: Option<&str>) -> Option<&'static str> {
let raw = platform?.trim();
if raw.is_empty() {
@@ -263,6 +274,7 @@ fn msvc_arch_flag(platform: Option<&str>) -> Option<&'static str> {
}
}
/// Returns default compiler flags for the specified build profile.
fn default_flags(profile: &str) -> &'static [&'static str] {
match profile {
"release" => &["/O2", "/DNDEBUG"],
@@ -271,6 +283,7 @@ fn default_flags(profile: &str) -> &'static [&'static str] {
}
}
/// Compiles all sources into object files using parallel processing.
fn build_objects(
compiler: &str,
sources: &[String],
@@ -292,6 +305,7 @@ fn build_objects(
Ok(objects)
}
/// Compiles a single source file to an object file.
fn build_object(
compiler: &str,
source: &str,
@@ -364,6 +378,7 @@ fn build_object(
let mut headers = Vec::new();
let mut clean_stdout = String::new();
// Parse compiler output to extract include file notes for dependency generation
for line in stdout.lines() {
if let Some(stripped) = line.strip_prefix("Note: including file:") {
headers.push(stripped.trim().to_string());

View File

@@ -15,12 +15,22 @@
// 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};
/// Computes a build fingerprint by hashing context fields and file metadata (SHA-256).
///
/// Includes `dcr.toml` (required) and `dcr.lock` when present, plus path/size/mtime
/// for each source, header, and library file.
///
/// # Parameters
/// - `ctx`: Build context (profile, tools, flags, dirs, …).
/// - `sources` / `headers` / `lib_files`: Inputs whose metadata enters the hash.
///
/// # Returns
/// Hex-encoded fingerprint, or an error if `dcr.toml` or a listed file cannot be read.
pub(crate) fn compute_build_fingerprint(
ctx: &BuildContext,
sources: &[String],
@@ -61,12 +71,14 @@ pub(crate) fn compute_build_fingerprint(
for value in ctx.ldflags {
hasher.update(value.as_bytes());
}
// Include project metadata from dcr.toml in the fingerprint
let toml =
fs::read_to_string("dcr.toml").map_err(|err| format!("Failed to read dcr.toml: {err}"))?;
hasher.update(toml.as_bytes());
if let Ok(lock) = fs::read_to_string("dcr.lock") {
hasher.update(lock.as_bytes());
}
// Include source, header, and library files in the fingerprint
for source in sources {
let path = Path::new(source);
update_hasher_with_file(&mut hasher, path)?;
@@ -80,6 +92,14 @@ pub(crate) fn compute_build_fingerprint(
Ok(to_hex(&hasher.finalize()))
}
/// Whether the build can be skipped: main artifact exists and cache hash matches.
///
/// # Parameters
/// - `ctx`: Used to locate the output artifact and `.dcr-build.hash`.
/// - `fingerprint`: Fresh hash from [`compute_build_fingerprint`].
///
/// # Returns
/// `true` only when both the artifact and matching cache file are present.
pub(crate) fn should_skip_build(ctx: &BuildContext, fingerprint: &str) -> bool {
let output = build_output_path(ctx);
if !Path::new(&output).is_file() {
@@ -90,6 +110,7 @@ pub(crate) fn should_skip_build(ctx: &BuildContext, fingerprint: &str) -> bool {
cached.trim() == fingerprint
}
/// Persists the build fingerprint to the cache file on disk.
pub(crate) fn write_build_fingerprint(ctx: &BuildContext, fingerprint: &str) -> Result<(), String> {
let cache_path = build_cache_path(ctx.profile, ctx.target_dir);
if let Some(parent) = cache_path.parent() {
@@ -99,6 +120,7 @@ pub(crate) fn write_build_fingerprint(ctx: &BuildContext, fingerprint: &str) ->
.map_err(|err| format!("Failed to write cache: {err}"))
}
/// Returns the path to the build cache hash file based on profile and target directory.
fn build_cache_path(profile: &str, target_dir: Option<&str>) -> PathBuf {
match target_dir {
Some(dir) => Path::new(dir).join(".dcr-build.hash"),
@@ -106,35 +128,26 @@ fn build_cache_path(profile: &str, target_dir: Option<&str>) -> PathBuf {
}
}
fn build_output_path(ctx: &BuildContext) -> String {
if crate::utils::build::is_flat_bin(ctx.kind) {
return crate::core::build::builder::artifact::flat_output_path(ctx);
}
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
format!("{}.{}", name, ext)
};
if ctx.kind == "staticlib" {
return crate::platform::lib_path(ctx.profile, &final_name, ctx.target_dir);
}
if ctx.kind == "sharedlib" {
return crate::platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir);
}
if ctx.kind == "efi" {
return crate::platform::efi_path(ctx.profile, &final_name, ctx.target_dir);
}
if ctx.kind == "elf" {
return crate::platform::elf_path(ctx.profile, &final_name, ctx.target_dir);
}
crate::platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
/// Resolves the output path for the build artifact using the provided context.
pub fn build_output_path(ctx: &BuildContext) -> String {
crate::core::build::builder::artifact::resolve_artifact_path(
ctx.kind,
ctx.profile,
ctx.project_name,
ctx.target_dir,
ctx.output_filename,
ctx.output_extension,
)
.unwrap_or_else(|| {
crate::platform::bin_path(
ctx.profile,
ctx.output_filename.unwrap_or(ctx.project_name),
ctx.target_dir,
)
})
}
/// Collects header files from source roots and include directories, filtering out excluded paths.
pub(crate) fn collect_header_files(
ctx: &BuildContext,
project_root: &Path,
@@ -176,6 +189,7 @@ pub(crate) fn collect_header_files(
Ok(out)
}
/// Recursively scans a directory to collect header files, respecting exclusion rules.
fn collect_header_files_rec(
dir: &Path,
out: &mut Vec<PathBuf>,
@@ -208,11 +222,13 @@ fn collect_header_files_rec(
Ok(())
}
/// Checks if the given path has a header file extension.
fn is_header_file(path: &Path) -> bool {
let ext = path.extension().and_then(|v| v.to_str()).unwrap_or("");
matches!(ext, "h" | "hpp" | "hh" | "hxx" | "inc")
}
/// Collects library files from lib directories using platform-specific candidates.
pub(crate) fn collect_lib_files(ctx: &BuildContext) -> Vec<PathBuf> {
let mut out = Vec::new();
for dir in ctx.lib_dirs {
@@ -229,6 +245,7 @@ pub(crate) fn collect_lib_files(ctx: &BuildContext) -> Vec<PathBuf> {
out
}
/// Candidate library filenames for the **host** OS (`cfg!(target_os = …)`).
fn lib_candidates(name: &str) -> Vec<String> {
if cfg!(target_os = "windows") {
return vec![format!("{name}.lib")];
@@ -247,6 +264,7 @@ fn lib_candidates(name: &str) -> Vec<String> {
]
}
/// Updates the hasher with the file's path, size, and last modification time.
fn update_hasher_with_file(hasher: &mut Sha256, path: &Path) -> Result<(), String> {
hasher.update(path.to_string_lossy().as_bytes());
let meta = fs::metadata(path).map_err(|err| format!("source read error: {err}"))?;
@@ -259,6 +277,7 @@ fn update_hasher_with_file(hasher: &mut Sha256, path: &Path) -> Result<(), Strin
Ok(())
}
/// Converts a byte slice containing the SHA256 hash to a hexadecimal string.
fn to_hex(bytes: &[u8]) -> String {
crate::utils::fs::to_hex(bytes)
}

View File

@@ -27,18 +27,25 @@ static OUTPUT_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
static PROGRESS_LABEL: OnceLock<Mutex<Option<String>>> = OnceLock::new();
static PROGRESS_DIRTY: AtomicBool = AtomicBool::new(false);
/// Returns a static mutex used to synchronize access to stderr during progress reporting.
/// This ensures thread-safe output when multiple build threads write to the terminal.
pub fn get_output_lock() -> &'static Mutex<()> {
OUTPUT_MUTEX.get_or_init(|| Mutex::new(()))
}
/// Initializes the shared progress label storage.
fn progress_label_lock() -> &'static Mutex<Option<String>> {
PROGRESS_LABEL.get_or_init(|| Mutex::new(None))
}
/// Sets the label text that will be shown next to the compile progress counter.
/// The label is cleared after the progress line is finished.
pub fn set_progress_label(label: Option<String>) {
*progress_label_lock().lock().unwrap() = label;
}
/// Finishes the current progress line by clearing the label and flushing the line if it was dirty.
/// This is called when a build step completes normally.
pub fn finish_progress_line() {
let was_dirty = PROGRESS_DIRTY.swap(false, Ordering::SeqCst);
*progress_label_lock().lock().unwrap() = None;
@@ -52,6 +59,8 @@ pub fn finish_progress_line() {
}
}
/// Interrupts the current progress line (if any) and flushes it to stderr.
/// Used when a build step produces output that should interrupt the progress display.
pub fn interrupt_progress_for_output() {
if !PROGRESS_DIRTY.swap(false, Ordering::SeqCst) {
return;
@@ -63,6 +72,9 @@ pub fn interrupt_progress_for_output() {
}
}
/// Reports the current compile progress to stderr in the format "label [done/total]".
/// Only prints if the terminal is attached and the label is set.
/// Updates the dirty flag so the line can be overwritten on the next call.
fn report_compile_progress(done: usize, total: usize) {
let label = progress_label_lock().lock().unwrap().clone();
let Some(label) = label else {
@@ -82,6 +94,9 @@ fn report_compile_progress(done: usize, total: usize) {
PROGRESS_DIRTY.store(true, Ordering::SeqCst);
}
/// Recursively collects all source files under the given roots that match the provided extensions.
/// Directories listed in exclude_dirs are skipped unless overridden by include_paths.
/// Returns a sorted list of normalized source paths or an error string.
pub fn collect_sources(
roots: &[PathBuf],
extensions: &[&str],
@@ -104,6 +119,8 @@ pub fn collect_sources(
Ok(sources)
}
/// Recursively walks a directory tree to collect source files.
/// Skips excluded directories unless they are explicitly allowed by include_paths.
fn collect_sources_rec(
dir: &Path,
extensions: &[&str],
@@ -151,6 +168,8 @@ fn collect_sources_rec(
Ok(())
}
/// Determines whether a path should be excluded from source collection.
/// Returns false if the path is explicitly allowed by include_paths (even if it matches an exclude).
pub fn is_excluded(path: &Path, exclude_dirs: &[PathBuf], include_paths: &[String]) -> bool {
if matches_patterns(path, include_paths, true) {
return false;
@@ -158,9 +177,33 @@ pub fn is_excluded(path: &Path, exclude_dirs: &[PathBuf], include_paths: &[Strin
if matches_patterns(path, include_paths, false) {
return true;
}
exclude_dirs.iter().any(|dir| path.starts_with(dir))
exclude_dirs.iter().any(|dir| path_is_under(path, dir))
}
/// Checks if a path is under a given prefix, handling both absolute and relative paths
/// and normalizing Windows-style separators for cross-platform comparison.
fn path_is_under(path: &Path, prefix: &Path) -> bool {
if path.starts_with(prefix) {
return true;
}
match (fs::canonicalize(path), fs::canonicalize(prefix)) {
(Ok(p), Ok(pref)) => p.starts_with(pref),
_ => {
let p = path.to_string_lossy().replace('\\', "/");
let pref = prefix.to_string_lossy().replace('\\', "/");
if p == pref || p.starts_with(&format!("{pref}/")) {
return true;
}
let p = p.strip_prefix("/private").unwrap_or(&p);
let pref = pref.strip_prefix("/private").unwrap_or(&pref);
p == pref || p.starts_with(&format!("{pref}/"))
}
}
}
/// Matches a path against include/exclude glob and path patterns.
/// Positive patterns allow files; negative patterns (starting with !) exclude them.
/// Returns true if the path matches any rule.
fn matches_patterns(path: &Path, patterns: &[String], positive: bool) -> bool {
if patterns.is_empty() {
return false;
@@ -225,10 +268,20 @@ fn matches_patterns(path: &Path, patterns: &[String], positive: bool) -> bool {
false
}
/// Returns true if the string contains any glob magic characters (*, ?, or [).
pub fn has_glob_magic(value: &str) -> bool {
value.chars().any(|c| matches!(c, '*' | '?' | '['))
}
/// Returns a GCC/Clang `-x` language for assembly sources.
///
/// # Parameters
/// - `source`: Source file path (extension decides the dialect).
///
/// # Returns
/// - `Some("assembler-with-cpp")` for `.S`
/// - `Some("assembler")` for `.s` / `.asm`
/// - `None` for other extensions
pub fn asm_lang_flag(source: &str) -> Option<&'static str> {
let ext = Path::new(source).extension().and_then(|v| v.to_str())?;
match ext {
@@ -238,6 +291,8 @@ pub fn asm_lang_flag(source: &str) -> Option<&'static str> {
}
}
/// Returns the list of file extensions associated with the given language token(s).
/// Splits comma-separated tokens and falls back to .c if no language is recognized.
pub fn source_extensions(language: &str) -> Vec<&'static str> {
let mut out = Vec::new();
for part in language.split(',').map(|s| s.trim()) {
@@ -251,10 +306,13 @@ pub fn source_extensions(language: &str) -> Vec<&'static str> {
out
}
/// Returns elapsed time in seconds with two decimal places for display purposes.
pub fn elapsed_secs(start: Instant) -> f64 {
((start.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0
}
/// Spawns a fixed number of worker threads to parallelize a build task.
/// Reports progress after each task completes.
pub fn parallel_build<F>(total_tasks: usize, task_fn: F, max_jobs: usize) -> Result<(), String>
where
F: Fn(usize) -> Result<(), String> + Sync + Send,
@@ -311,6 +369,8 @@ where
Ok(())
}
/// Runs a command, prints stdout/stderr to the terminal (interrupting progress),
/// and returns `Ok(())` or `Err` based on exit status.
pub fn run_command_sync_output(cmd: &mut Command) -> Result<(), String> {
let program = cmd.get_program().to_string_lossy().into_owned();
let output = cmd.output().map_err(|err| {
@@ -338,6 +398,8 @@ pub fn run_command_sync_output(cmd: &mut Command) -> Result<(), String> {
}
}
/// Normalizes a source path to be relative to the current working directory when possible.
/// Returns the original path unchanged if it cannot be made relative.
pub fn normalize_source_path(path: &Path) -> String {
if !path.is_absolute() {
return path.to_string_lossy().to_string();
@@ -350,6 +412,8 @@ pub fn normalize_source_path(path: &Path) -> String {
path.to_string_lossy().to_string()
}
/// Computes the output object file path by stripping the leading ./ and adjusting the extension.
/// Places the object in the specified obj_dir while preserving directory structure.
pub fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
let src_path = Path::new(source);
let stripped = src_path.strip_prefix("./").unwrap_or(src_path);
@@ -379,9 +443,19 @@ pub fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
} else {
out.set_extension(obj_ext);
}
out.to_string_lossy().to_string()
out.to_string_lossy().replace('\\', "/")
}
/// Checks whether a source file needs to be rebuilt.
///
/// Compares mtimes of the source, object, and deps listed in the sibling `.d` file.
///
/// # Parameters
/// - `source`: Source path.
/// - `object`: Object path (`.d` is derived by changing the extension).
///
/// # Returns
/// `true` if the object is missing, older than source/deps, or a dep is missing.
pub fn needs_rebuild(source: &str, object: &str) -> bool {
let src_time = fs::metadata(source).and_then(|m| m.modified());
let obj_time = fs::metadata(object).and_then(|m| m.modified());
@@ -417,6 +491,14 @@ pub fn needs_rebuild(source: &str, object: &str) -> bool {
false
}
/// Checks whether the output executable needs to be relinked.
///
/// # Parameters
/// - `objects`: Object file paths that feed the link step.
/// - `output_path`: Linked artifact path.
///
/// # Returns
/// `true` if the output is missing or any object is newer than it.
pub fn needs_link(objects: &[String], output_path: &str) -> bool {
let out_meta = match fs::metadata(output_path) {
Ok(m) => m,
@@ -442,6 +524,8 @@ pub fn needs_link(objects: &[String], output_path: &str) -> bool {
false
}
/// Parses a dependency file (.d) produced by GCC or MSVC compilers.
/// Extracts all dependency paths, handling escaped newlines and Windows-style paths.
fn parse_d_file(content: &str) -> Vec<String> {
let mut deps = Vec::new();
let mut target_end = 0;
@@ -482,7 +566,11 @@ fn parse_d_file(content: &str) -> Vec<String> {
}
in_escape = false;
} else if c == '\\' {
in_escape = true;
match chars.peek() {
Some('\n' | '\r' | ' ' | '\t' | '\\') => in_escape = true,
Some(_) => current_path.push('\\'),
None => current_path.push('\\'),
}
} else if c.is_whitespace() {
if !current_path.is_empty() {
deps.push(current_path.clone());
@@ -509,12 +597,18 @@ mod tests {
static TEST_COUNTER: AtomicUsize = AtomicUsize::new(0);
static CWD_LOCK: Mutex<()> = Mutex::new(());
fn cwd_lock() -> std::sync::MutexGuard<'static, ()> {
CWD_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn temp_dir(prefix: &str) -> PathBuf {
let n = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let dir = std::env::temp_dir().join(format!("dcr_test_{prefix}_{n}"));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
dir
fs::canonicalize(&dir).unwrap_or(dir)
}
fn default_roots() -> Vec<PathBuf> {
@@ -679,7 +773,7 @@ mod tests {
fs::write(src.join("utils.c"), "").unwrap();
fs::write(src.join("README.md"), "").unwrap(); // should be ignored
let _guard = CWD_LOCK.lock().unwrap();
let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new();
@@ -703,7 +797,7 @@ mod tests {
fs::write(src.join("other.cc"), "").unwrap();
fs::write(src.join("skip.c"), "").unwrap(); // should be ignored
let _guard = CWD_LOCK.lock().unwrap();
let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new();
@@ -721,7 +815,7 @@ mod tests {
let src = dir.join("src");
fs::create_dir_all(&src).unwrap();
let _guard = CWD_LOCK.lock().unwrap();
let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new();
@@ -743,7 +837,7 @@ mod tests {
fs::write(vendor.join("lib.c"), "").unwrap(); // should be excluded
let exclude = vec![vendor.clone()];
let _guard = CWD_LOCK.lock().unwrap();
let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new();
@@ -765,7 +859,7 @@ mod tests {
fs::write(src.join("main.c"), "").unwrap();
fs::write(sub.join("nested.c"), "").unwrap();
let _guard = CWD_LOCK.lock().unwrap();
let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new();
@@ -789,7 +883,7 @@ mod tests {
let exclude = vec![boot.clone()];
let include = vec!["src/boot/arch/**".to_string()];
let _guard = CWD_LOCK.lock().unwrap();
let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let roots = default_roots();
@@ -816,7 +910,7 @@ mod tests {
"!src/legacy/**".to_string(),
"src/legacy/allow/**".to_string(),
];
let _guard = CWD_LOCK.lock().unwrap();
let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let roots = default_roots();
@@ -839,7 +933,7 @@ mod tests {
let exclude: Vec<PathBuf> = Vec::new();
let include = vec!["!src/gen/**".to_string()];
let _guard = CWD_LOCK.lock().unwrap();
let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let roots = default_roots();
@@ -874,36 +968,44 @@ mod tests {
#[test]
fn needs_rebuild_header_modified() {
let dir = temp_dir("rebuild_header");
let src = dir.join("test.c");
let header = dir.join("test.h");
let obj = dir.join("test.o");
let d_file = dir.join("test.d");
fs::write(dir.join("test.c"), "int main() {}").unwrap();
fs::write(dir.join("test.h"), "#define A 1").unwrap();
fs::write(dir.join("test.o"), "").unwrap();
fs::write(&src, "int main() {}").unwrap();
fs::write(&header, "#define A 1").unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
fs::write(&obj, "").unwrap();
let d_content = format!(
"{}: {} {}",
obj.to_string_lossy(),
src.to_string_lossy(),
header.to_string_lossy()
);
fs::write(&d_file, d_content).unwrap();
let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
fs::write("test.d", "test.o: test.c missing.h").unwrap();
assert!(
!needs_rebuild(&src.to_string_lossy(), &obj.to_string_lossy()),
"should be fresh"
needs_rebuild("test.c", "test.o"),
"missing dep in .d must force rebuild"
);
std::thread::sleep(std::time::Duration::from_millis(50));
fs::write(&header, "#define A 2").unwrap(); // modify header
fs::write("test.h", "#define A 1").unwrap();
std::thread::sleep(std::time::Duration::from_secs(2));
fs::write("test.o", "obj").unwrap();
fs::write("test.d", "test.o: test.c test.h").unwrap();
assert!(
needs_rebuild(&src.to_string_lossy(), &obj.to_string_lossy()),
!needs_rebuild("test.c", "test.o"),
"object newer than source and headers should be fresh"
);
std::thread::sleep(std::time::Duration::from_secs(2));
fs::write("test.h", "#define A 2").unwrap();
assert!(
needs_rebuild("test.c", "test.o"),
"changed header should trigger rebuild"
);
std::env::set_current_dir(prev).unwrap();
}
#[test]
fn parse_d_file_keeps_windows_path_separators() {
let content = r"target\obj\main.obj: src\main.c src\utils.h";
let deps = parse_d_file(content);
assert_eq!(deps, vec![r"src\main.c", r"src\utils.h"]);
}
#[test]

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/>.
/// Core build engine module for DCR.
/// This module provides the main build logic for projects and workspaces.
use crate::core::build::builder::{BuildContext, build as build_project, collect_sources};
use crate::core::build::cache::{
collect_header_files, collect_lib_files, compute_build_fingerprint, should_skip_build,
@@ -33,13 +35,14 @@ 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::{
get_bool_with_profile, get_config_opt, get_config_str, get_language_with_profile,
is_bare_metal_target, normalize_kind, normalize_platform, normalize_target,
default_target_triple, get_bool_with_profile, get_config_opt, get_config_str,
get_language_with_profile, is_bare_metal_target, normalize_kind, normalize_platform,
normalize_target_os, parse_version_info, prepend_clang_target_flag, primary_language,
resolve_compiler, resolve_pkg_config_flags, resolve_tool, substitute_vars,
resolve_artifact_target_dir, resolve_compiler, resolve_pkg_config_flags, resolve_tool,
substitute_vars,
};
use crate::utils::fs::check_dir;
use std::fs;
@@ -48,6 +51,7 @@ use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Instant;
/// Ensures that target directories exist for the build output.
fn ensure_target_dirs(
project_root: &Path,
items: &[String],
@@ -61,29 +65,13 @@ fn ensure_target_dirs(
if let Some(dir) = &target_dir {
let _ = fs::create_dir_all(dir);
} else {
let default_dir = if cfg!(target_os = "linux") {
let arch = std::env::consts::ARCH;
format!("{arch}-unknown-linux-gnu/{profile}")
} else if cfg!(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)) {
let arch = std::env::consts::ARCH;
let os = std::env::consts::OS;
format!("{arch}-unknown-{os}/{profile}")
} else {
profile.to_string()
};
let target_path = project_root.join("target");
let target_items = check_dir(target_path.to_str()).unwrap_or_default();
if !target_items.contains(&default_dir) {
let _ = fs::create_dir_all(target_path.join(&default_dir));
}
let _ = fs::create_dir_all(
project_root.join(crate::utils::build::native_host_target_rel(profile)),
);
}
}
/// Runs the project build and returns the elapsed time in seconds or an error.
fn run_compile(
ctx: &BuildContext,
_cancel: &Arc<AtomicBool>,
@@ -105,8 +93,16 @@ fn run_compile(
}
}
// Library entry point: run a build described by `req`, streaming progress to
// `rep`. The CLI wraps this with a CliReporter; TUI/GUI/MCP plug in their own.
/// Library entry point: run a build described by `req`, streaming progress to
/// `rep`. The CLI wraps this with a CliReporter; TUI/GUI/MCP plug in their own.
///
/// # Parameters
/// - `root`: Project or workspace root containing `dcr.toml`.
/// - `req`: Profile, target, force/verbose, optional member, cancel flag.
/// - `rep`: Progress sink for build events.
///
/// # Returns
/// `BuildOutcome` with elapsed seconds, or `BuildError` on failure/cancel.
pub fn run_build(
root: &Path,
req: &BuildRequest,
@@ -126,6 +122,7 @@ pub fn run_build(
Ok(BuildOutcome { secs })
}
/// Orchestrates the build for the specified profile and target, handling workspaces and projects.
#[allow(unused_variables, clippy::too_many_arguments)]
fn build_all(
root: &Path,
@@ -159,6 +156,7 @@ fn build_all(
&member.path,
profile,
target,
target.is_some_and(|t| !t.trim().is_empty()),
&[],
force,
verbose,
@@ -186,22 +184,34 @@ fn build_all(
let project_name = get_config_str(&config, "package.name");
let project_version = get_config_str(&config, "package.version");
let targets_to_build: Vec<Option<String>> = if let Some(t) = target {
vec![Some(normalize_target_os(t).to_string())]
let targets_to_build: Vec<(Option<String>, bool)> = if let Some(t) = target {
let normalized = normalize_target_os(t);
if normalized.is_empty() {
vec![(Some(default_target_triple()), true)]
} else {
vec![(Some(normalized.to_string()), true)]
}
} else {
let config_targets = get_targets(&config, profile)?;
if config_targets.is_empty() {
vec![None]
vec![(Some(default_target_triple()), false)]
} else {
config_targets
.into_iter()
.map(|t| Some(normalize_target_os(&t).to_string()))
.map(|t| {
let normalized = normalize_target_os(&t);
if normalized.is_empty() {
(Some(default_target_triple()), true)
} else {
(Some(normalized.to_string()), true)
}
})
.collect()
}
};
let start_time = Instant::now();
for (i, build_target) in targets_to_build.iter().enumerate() {
for (i, (build_target, explicit_target)) in targets_to_build.iter().enumerate() {
if cancel.load(Ordering::SeqCst) {
return Err("Build interrupted".to_string());
}
@@ -226,6 +236,7 @@ fn build_all(
&member.path,
profile,
build_target.as_deref(),
*explicit_target,
&[],
force,
verbose,
@@ -239,6 +250,7 @@ fn build_all(
&ws,
profile,
build_target.as_deref(),
*explicit_target,
force,
verbose,
Some(root),
@@ -251,6 +263,7 @@ fn build_all(
root,
profile,
build_target.as_deref(),
*explicit_target,
&excludes,
force,
verbose,
@@ -264,6 +277,7 @@ fn build_all(
root,
profile,
build_target.as_deref(),
*explicit_target,
&[],
force,
verbose,
@@ -278,11 +292,13 @@ fn build_all(
Ok(elapsed)
}
/// Builds each workspace member in order (flat loop, not recursive into nested workspaces).
#[allow(clippy::too_many_arguments)]
fn build_workspace(
workspace: &crate::core::workspace::Workspace,
profile: &str,
target: Option<&str>,
explicit_target: bool,
force: bool,
verbose: bool,
workspace_root: Option<&Path>,
@@ -294,6 +310,7 @@ fn build_workspace(
&member.path,
profile,
target,
explicit_target,
&[],
force,
verbose,
@@ -305,17 +322,94 @@ fn build_workspace(
Ok(())
}
/// Builds a single project at the given root, handling configuration and steps.
#[allow(clippy::too_many_arguments)]
fn build_project_at(
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,
) -> 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());
@@ -344,12 +438,43 @@ fn build_project_at(
}
let project_name = get_config_str(&config, "package.name");
let project_version = get_config_str(&config, "package.version");
let host_target = default_target_triple();
let build_target_config = get_build_string_with_profile(&config, "target", profile);
let build_target = target.or(if build_target_config.is_empty() {
None
} else {
Some(build_target_config.as_str())
});
let has_explicit_target = explicit_target || !build_target_config.is_empty();
let explicit_build_target = explicit_target
.then_some(target)
.flatten()
.filter(|t| !t.trim().is_empty())
.map(|t| {
let normalized = normalize_target_os(t);
if normalized.is_empty() {
host_target.clone()
} else {
normalized.to_string()
}
});
let configured_build_target = || {
if build_target_config.is_empty() {
None
} else {
let normalized = normalize_target_os(&build_target_config);
if normalized.is_empty() {
None
} else {
Some(normalized.to_string())
}
}
};
let implicit_build_target = || {
target
.filter(|t| !t.trim().is_empty())
.map(|t| normalize_target_os(t).to_string())
};
let build_target_owned = explicit_build_target
.or_else(configured_build_target)
.or_else(implicit_build_target)
.unwrap_or(host_target);
let build_target = Some(build_target_owned.as_str());
let build_language = get_language_with_profile(&config, profile)?;
let build_qt = get_bool_with_profile(&config, "qt", profile, false);
let lang_key = primary_language(&build_language);
@@ -484,77 +609,27 @@ fn build_project_at(
tc_as.as_deref(),
);
let resolved_linker = resolve_tool("DCR_LD", tc_ld.as_deref());
prepend_clang_target_flag(&mut build_cflags, build_target, &resolved_compiler);
// The host triple is still used for native target-specific configuration and
// artifact paths, but native Clang must select the platform SDK implicitly.
// Pass --target only when the user/config explicitly requested a target.
let compiler_target = if explicit_target { build_target } else { None };
prepend_clang_target_flag(&mut build_cflags, compiler_target, &resolved_compiler);
prepend_clang_target_flag(
&mut build_ldflags,
build_target,
compiler_target,
resolved_linker.as_deref().unwrap_or(&resolved_compiler),
);
let resolved_archiver = resolve_tool("DCR_AR", tc_ar.as_deref());
let out_dir_config = get_build_string_with_profile(&config, "out_dir", profile);
let target_dir_binding = if !out_dir_config.is_empty() {
let p = Path::new(&out_dir_config);
if p.is_absolute() {
out_dir_config
} else {
project_root.join(p).to_string_lossy().to_string()
}
} else {
match workspace_root {
Some(root) => {
let target_str = build_target
.map(normalize_target_os)
.filter(|t| !t.is_empty());
let rel: PathBuf = match target_str {
Some(ref t) => Path::new("target").join(t).join(profile),
None => {
let default_dir = if cfg!(target_os = "linux") {
let arch = std::env::consts::ARCH;
format!("{arch}-unknown-linux-gnu/{profile}")
} else if cfg!(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)) {
let arch = std::env::consts::ARCH;
let os = std::env::consts::OS;
format!("{arch}-unknown-{os}/{profile}")
} else {
profile.to_string()
};
Path::new("target").join(&default_dir)
}
};
root.join(&rel).to_string_lossy().to_string()
}
None => {
let base_dir =
if let Some(rel) = normalize_target(build_target.unwrap_or(""), profile) {
PathBuf::from(rel)
} else {
let default_dir = if cfg!(target_os = "linux") {
let arch = std::env::consts::ARCH;
format!("{arch}-unknown-linux-gnu/{profile}")
} else if cfg!(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)) {
let arch = std::env::consts::ARCH;
let os = std::env::consts::OS;
format!("{arch}-unknown-{os}/{profile}")
} else {
profile.to_string()
};
Path::new("target").join(&default_dir)
};
project_root.join(base_dir).to_string_lossy().to_string()
}
}
};
let target_dir_binding = resolve_artifact_target_dir(
project_root,
workspace_root,
profile,
&build_target_owned,
&out_dir_config,
has_explicit_target,
);
let target_dir = if target_dir_binding.is_empty() {
None
} else {
@@ -612,24 +687,9 @@ fn build_project_at(
let target_str = build_target
.map(normalize_target_os)
.filter(|t| !t.is_empty());
let default_dir = if cfg!(target_os = "linux") {
let arch = std::env::consts::ARCH;
format!("{arch}-unknown-linux-gnu/{profile}")
} else if cfg!(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)) {
let arch = std::env::consts::ARCH;
let os = std::env::consts::OS;
format!("{arch}-unknown-{os}/{profile}")
} else {
profile.to_string()
};
let rel = match target_str {
Some(ref t) => Path::new("target").join(t).join(profile),
None => Path::new("target").join(&default_dir),
None => crate::utils::build::native_host_target_rel(profile),
};
let dep_build_lib1 = dep_member.path.join(&rel);
let dep_build_lib2 = wroot.join(&rel);
@@ -660,65 +720,63 @@ 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()
));
}
build_project_at(
&dep_root,
profile,
build_target,
&[],
force,
verbose,
None,
cancel,
&mut *rep,
)?;
rep.on_event(BuildEvent::DepReady {
name,
version,
rebuilt: true,
});
} else {
rep.on_event(BuildEvent::DepReady {
name,
version,
rebuilt: false,
});
}
}
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;
}
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,
&[],
force,
verbose,
None,
cancel,
&mut *rep,
dependency_stack,
)?;
rep.on_event(BuildEvent::DepReady {
name: &name,
version: &version,
rebuilt: true,
});
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());
@@ -937,6 +995,43 @@ 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 {
if let Some(rest) = flag.strip_prefix(prefix) {
@@ -950,12 +1045,14 @@ fn absolutize_flag_path(flag: &str, project_root: &Path, prefixes: &[&str]) -> S
return flag.to_string();
}
let abs = project_root.join(rest);
return format!("{prefix}{}", abs.to_string_lossy());
let path = abs.to_string_lossy().replace('\\', "/");
return format!("{prefix}{path}");
}
}
flag.to_string()
}
/// Finds the nearest ancestor directory named "target" starting from the given dir.
fn find_target_root(dir: &str, fallback: &Path) -> PathBuf {
let p = Path::new(dir);
for ancestor in p.ancestors() {
@@ -966,6 +1063,7 @@ fn find_target_root(dir: &str, fallback: &Path) -> PathBuf {
fallback.to_path_buf()
}
/// Packages the built library by copying headers and libs to the target include and lib directories.
fn package_library(
ctx: &BuildContext,
headers: &[PathBuf],
@@ -1020,12 +1118,14 @@ fn package_library(
Ok(())
}
/// Holds the resolved executable paths for Qt tools.
pub(crate) struct ToolchainExecs {
pub(crate) uic: String,
pub(crate) moc: String,
pub(crate) rcc: String,
}
/// Resolves the paths for uic, moc, and rcc tools, preferring configured values or pkg-config/Qt6 variants.
fn resolve_toolchain_execs(
uic: &Option<String>,
moc: &Option<String>,
@@ -1040,6 +1140,7 @@ fn resolve_toolchain_execs(
}
}
/// Resolves a Qt tool path, falling back to Qt6 variants if not found in standard locations.
fn resolve_qt_tool(configured: &Option<String>, qt_bins: Option<&Path>, tool: &str) -> String {
if let Some(value) = configured {
return value.clone();
@@ -1062,6 +1163,7 @@ fn resolve_qt_tool(configured: &Option<String>, qt_bins: Option<&Path>, tool: &s
tool.to_string()
}
/// Queries pkg-config for Qt6 host bin directories for the given packages.
fn resolve_qt_host_bins(pkgs: &[String]) -> Option<PathBuf> {
let qt_pkgs: Vec<&String> = pkgs.iter().filter(|p| p.starts_with("Qt6")).collect();
if qt_pkgs.is_empty() {
@@ -1097,6 +1199,7 @@ fn resolve_qt_host_bins(pkgs: &[String]) -> Option<PathBuf> {
None
}
/// Executes pkg-config to get a variable value for a package and returns the path if valid.
fn query_pkg_config_var(pkg: &str, var: &str) -> Option<PathBuf> {
let output = std::process::Command::new("pkg-config")
.arg(format!("--variable={var}"))
@@ -1114,6 +1217,7 @@ fn query_pkg_config_var(pkg: &str, var: &str) -> Option<PathBuf> {
if path.is_dir() { Some(path) } else { None }
}
/// Extracts tool binaries from a libexec directory or its bin subdirectory.
fn qt_bins_from_libexec(libexec: &Path) -> Option<PathBuf> {
for tool in ["moc", "uic", "rcc"] {
if libexec.join(tool).is_file() {
@@ -1127,12 +1231,14 @@ fn qt_bins_from_libexec(libexec: &Path) -> Option<PathBuf> {
if bin.is_dir() { Some(bin) } else { None }
}
/// Detects if a Qt6 tool variant (like uic6 or uic-qt6) is available on the PATH.
fn detect_qt6_tool_variant(tool: &str) -> Option<String> {
[format!("{tool}6"), format!("{tool}-qt6")]
.into_iter()
.find(|candidate| is_on_path(candidate))
}
/// Checks if a command is available either as a file or in the PATH environment variable.
fn is_on_path(cmd: &str) -> bool {
if Path::new(cmd).is_file() {
return true;

View File

@@ -1,10 +1,14 @@
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.
///
/// # Parameters
/// - `flags`: Full compiler flag list (often shared with C/C++).
///
/// # Returns
/// Cloned flags that start with `-I`/`-i` or equal `-g`.
pub fn filter_asm_flags(flags: &[String]) -> Vec<String> {
flags
.iter()
@@ -16,6 +20,19 @@ pub fn filter_asm_flags(flags: &[String]) -> Vec<String> {
.collect()
}
/// Builds assembly sources with direct flat emission when `kind` is `flat-bin`.
///
/// For non-flat kinds, assembles to objects and then links/archives as usual.
///
/// # Parameters
/// - `ctx`: Build context (kind, sources, flags, …).
/// - `backend_name`: Label used in error messages (e.g. `"NASM"`).
/// - `default_assembler`: Assembler binary when `ctx.compiler` is empty.
/// - `extensions`: Source extensions to collect.
/// - `build_object`: Per-source assemble callback `(assembler, src, out, ctx)`.
///
/// # Returns
/// Elapsed seconds, or an error string.
pub fn build_assembly<F>(
ctx: &BuildContext,
backend_name: &str,
@@ -36,6 +53,15 @@ where
)
}
/// Builds assembly sources; for `flat-bin`, assembles to objects then runs objcopy.
///
/// For non-flat kinds, assembles to objects and then links/archives as usual.
///
/// # Parameters
/// - Same as [`build_assembly`]; only the flat emission strategy differs.
///
/// # Returns
/// Elapsed seconds, or an error string.
pub fn build_assembly_via_objcopy<F>(
ctx: &BuildContext,
backend_name: &str,
@@ -56,12 +82,14 @@ where
)
}
/// Flat-bin emission strategy: direct assembler output, or object then objcopy.
#[derive(Clone, Copy)]
enum FlatEmit {
Direct,
ViaObjcopy,
}
/// Core assembly build with a chosen flat emission mode; `build_object` runs per source in parallel.
fn build_assembly_with_mode<F>(
ctx: &BuildContext,
backend_name: &str,
@@ -74,6 +102,7 @@ where
F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync,
{
let lang = ctx.language.to_lowercase();
// Validate language
if lang.split(',').any(|p| {
let t = p.trim();
t != "asm" && t != "llvm_ir" && t != "llvm-ir" && t != "ll"
@@ -104,6 +133,7 @@ where
};
let objects = match (flat, flat_mode) {
// Dispatch based on flat binary kind and emission mode
(true, FlatEmit::Direct) => {
let outs: Vec<String> = sources
.iter()
@@ -148,6 +178,7 @@ where
artifact::link_binary(ctx, &objects, "cc", start_time)
}
/// Compiles all sources to objects in parallel (non-flat path) and returns object paths.
fn build_objects_common<F>(
assembler: &str,
sources: &[String],

View File

@@ -1,14 +1,15 @@
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.
///
/// Delegates to the shared assembly build logic with FASM configuration.
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
asm::build_assembly(ctx, "FASM", "fasm", &["asm", "fasm"], build_object)
}
/// Collects all source files for assembly files using FASM.
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
common::collect_sources(
ctx.source_roots,
@@ -18,6 +19,7 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
)
}
/// Assembles one FASM source into an object file.
fn build_object(
assembler: &str,
source: &str,
@@ -28,6 +30,7 @@ fn build_object(
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
// Skip rebuilding if the object is up to date
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}

View File

@@ -15,13 +15,14 @@
// 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.
///
/// Resolves `as` on PATH and delegates to the shared assembly builder. For
/// `flat-bin`, intermediates are converted with objcopy after assembly.
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
// Use full path to as
let as_path = std::process::Command::new("which")
@@ -35,6 +36,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
asm::build_assembly_via_objcopy(ctx, "GAS", &as_path, &["s"], build_object)
}
/// Collects assembly source files with .s extension from the build context's source roots.
///
/// Excludes specified directories and respects include paths for filtering.
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
common::collect_sources(
ctx.source_roots,
@@ -44,6 +48,10 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
)
}
/// Assembles a single .s source file into an object file using the provided assembler.
///
/// Creates parent directories for the output object, skips if up-to-date,
/// runs the assembler with flags from the build context, and handles verbose output.
fn build_object(
assembler: &str,
source: &str,
@@ -54,10 +62,12 @@ fn build_object(
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
// Check if the object file is already up to date to avoid unnecessary rebuilds.
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
// Prepare the assembler command with source file and output path.
let mut cmd = Command::new(assembler);
cmd.arg(source).arg("-o").arg(obj_path);

View File

@@ -1,14 +1,16 @@
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`.
///
/// For `flat-bin`, intermediates are converted with objcopy after assembly.
/// Returns elapsed seconds or an error.
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
asm::build_assembly_via_objcopy(ctx, "MASM", "ml", &["asm"], build_object)
}
/// Collects MASM assembly source files from the given source roots.
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
common::collect_sources(
ctx.source_roots,
@@ -18,6 +20,12 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
)
}
/// Builds a single MASM object file from the given source.
///
/// Creates the parent directory for the output object if it doesn't exist,
/// skips the build if the object is already up to date, constructs the ml
/// command line with flags, prints the command in debug mode if enabled,
/// and executes it.
fn build_object(
assembler: &str,
source: &str,
@@ -25,10 +33,12 @@ fn build_object(
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
// Ensure the parent directory exists for the object file
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
// Skip rebuild if the object file is already current
return Ok(());
}
@@ -40,6 +50,7 @@ fn build_object(
.arg(source);
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
// Add filtered compiler flags to the command
cmd.arg(flag);
}

View File

@@ -15,6 +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/>.
/// Assembly language module for the DCR build system.
pub mod common;
pub mod fasm;
pub mod gas;
@@ -23,15 +24,21 @@ pub mod nasm;
use crate::core::build::language::Language;
/// Represents the assembly language support in the build system.
pub struct Asm;
impl Language for Asm {
/// Returns the identifier string for this language.
fn id(&self) -> &'static str {
"asm"
}
/// Returns the list of supported file extensions for assembly files.
fn extensions(&self) -> &'static [&'static str] {
&["s", "S", "asm"]
}
/// Checks whether the given token matches the assembly language, using case-insensitive comparison.
fn matches_token(&self, token: &str) -> bool {
token.eq_ignore_ascii_case("asm")
}

View File

@@ -15,17 +15,21 @@
// 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;
/// Support for NASM assembly language in the DCR build system.
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.
///
/// Delegates to the shared assembly builder with NASM-specific configuration.
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
asm::build_assembly(ctx, "NASM", "nasm", &["asm"], build_object)
}
/// Collects source files for NASM assembly.
///
/// Filters for .asm files only, as NASM uses that extension (distinct from GAS .s).
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
// NASM handles .asm only (not .s which is GAS syntax, not .S which is GCC preprocessed)
common::collect_sources(
@@ -36,6 +40,9 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
)
}
/// Builds a single object file from an assembly source using NASM.
///
/// Handles directory creation, rebuild checks, format selection, and command execution.
fn build_object(
assembler: &str,
source: &str,
@@ -43,13 +50,16 @@ fn build_object(
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
// Ensure output directory exists before building.
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
// Skip building if the object is already up to date.
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
// Select appropriate object format for the target platform and binary type.
let format = if crate::utils::build::is_flat_bin(ctx.kind) {
"bin"
} else {
@@ -63,6 +73,7 @@ fn build_object(
.arg("-o")
.arg(obj_path);
// Append any additional assembly flags from build context.
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
cmd.arg(flag);
}
@@ -74,6 +85,9 @@ fn build_object(
common::run_command_sync_output(&mut cmd)
}
/// Selects the NASM object file format string based on the target platform.
///
/// Supports different formats for x86 on various OSes and architectures.
fn nasm_format(platform: Option<&str>) -> &'static str {
if let Some(p) = platform {
let p = p.to_lowercase().replace('-', "_");

View File

@@ -17,15 +17,21 @@
use crate::core::build::language::Language;
/// Represents the C programming language support within the DCR build system.
pub struct C;
impl Language for C {
/// Returns the unique identifier for the C language.
fn id(&self) -> &'static str {
"c"
}
/// Returns an array containing the file extension associated with C source code.
fn extensions(&self) -> &'static [&'static str] {
&["c"]
}
/// Checks whether the given token matches the C language identifier in a case-insensitive manner.
fn matches_token(&self, token: &str) -> bool {
token.eq_ignore_ascii_case("c")
}

View File

@@ -15,20 +15,26 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
/// C++ language support for the DCR build system.
pub mod qt;
use crate::core::build::language::Language;
pub struct Cxx;
/// C++ language implementation.
impl Language for Cxx {
/// Returns the language ID "cxx".
fn id(&self) -> &'static str {
"cxx"
}
/// Returns the extensions for C++ files.
fn extensions(&self) -> &'static [&'static str] {
&["cpp", "cxx", "cc"]
}
/// Checks if the token matches C++ language names, ignoring case.
fn matches_token(&self, token: &str) -> bool {
// Case-insensitive comparison using to_lowercase
matches!(token.to_lowercase().as_str(), "c++" | "cpp" | "cxx")
}
}

View File

@@ -15,13 +15,22 @@
// 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};
use std::process::Command;
/// Runs uic/rcc/moc on Qt UI, resource, and header sources under `build_dir/qt`.
///
/// # Parameters
/// - `ctx`: Sources, excludes, and toolchain tool paths.
/// - `build_dir`: Directory that will contain the generated `qt/` tree.
///
/// # Returns
/// - `Ok(Some(qt_dir))` if any generated files were produced.
/// - `Ok(None)` if there was nothing to process.
/// - `Err` if a Qt tool fails.
pub fn process_qt(ctx: &BuildContext, build_dir: &Path) -> Result<Option<PathBuf>, String> {
let qt_dir = build_dir.join("qt");
fs::create_dir_all(&qt_dir).map_err(|e| format!("Failed to create qt dir: {e}"))?;
@@ -37,6 +46,7 @@ pub fn process_qt(ctx: &BuildContext, build_dir: &Path) -> Result<Option<PathBuf
let mut qt_moc_args = Vec::new();
let qt_modules = vec!["Qt6Core", "Qt6Widgets", "Qt6Gui", "Qt6Svg"];
// Collect -I and -D flags from pkg-config for Qt modules to use in moc invocations
for module in qt_modules {
if let Ok(cflags) = run_pkg_config(module, "--cflags") {
for flag in cflags.split_whitespace() {
@@ -123,6 +133,7 @@ pub fn process_qt(ctx: &BuildContext, build_dir: &Path) -> Result<Option<PathBuf
}
}
/// Executes the specified command with the given arguments and returns success or an error message.
fn run_command(cmd_path: &str, args: &[&str], verbose: bool) -> Result<(), String> {
let mut cmd = Command::new(cmd_path);
cmd.args(args);

View File

@@ -1,29 +1,33 @@
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;
impl Language for LlvmIr {
/// Returns the unique identifier for the LLVM IR language.
fn id(&self) -> &'static str {
"llvm_ir"
}
/// Returns the list of file extensions recognized for this language.
fn extensions(&self) -> &'static [&'static str] {
&["ll"]
}
/// Determines whether the provided token matches the LLVM IR language.
fn matches_token(&self, token: &str) -> bool {
matches!(token.to_lowercase().as_str(), "llvm_ir" | "llvm-ir" | "ll")
}
}
/// Builds LLVM IR source files into object files using llc.
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
asm::build_assembly_via_objcopy(ctx, "LLVM-IR", "llc", &["ll"], build_object)
}
/// Collects source files for LLVM IR compilation from the given context.
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
common::collect_sources(
ctx.source_roots,
@@ -33,6 +37,7 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
)
}
/// Builds a single LLVM IR source file into an object file.
fn build_object(
assembler: &str,
source: &str,
@@ -40,6 +45,7 @@ fn build_object(
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
// Create parent directory if it does not exist
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}

View File

@@ -15,21 +15,29 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
/// Core module for language support in the DCR build system.
/// Defines the Language trait and helper functions to register and lookup languages.
pub mod asm;
pub mod c;
pub mod cxx;
pub mod llvm_ir;
/// Trait representing a supported programming language.
/// Each implementation must specify its unique identifier, list of supported file extensions,
/// and a method to determine if a given token matches the language.
pub trait Language {
fn id(&self) -> &'static str;
fn extensions(&self) -> &'static [&'static str];
fn matches_token(&self, token: &str) -> bool;
}
/// Returns a fixed-size array containing static references to all supported language types.
pub fn languages() -> [&'static dyn Language; 4] {
[&c::C, &cxx::Cxx, &asm::Asm, &llvm_ir::LlvmIr]
}
/// Looks up the language implementation that matches the given token by checking each language's
/// matches_token method. Returns the first match or None if no language matches.
pub fn language_for_token(token: &str) -> Option<&'static dyn Language> {
languages().into_iter().find(|l| l.matches_token(token))
}
@@ -38,6 +46,7 @@ pub fn language_for_token(token: &str) -> Option<&'static dyn Language> {
mod tests {
use super::*;
/// Ensures that input tokens are resolved to the correct language IDs.
#[test]
fn tokens_resolve_to_languages() {
assert_eq!(language_for_token("c").unwrap().id(), "c");
@@ -48,6 +57,7 @@ mod tests {
assert!(language_for_token("rust").is_none());
}
/// Ensures that each language reports the expected set of file extensions.
#[test]
fn languages_expose_expected_extensions() {
assert_eq!(c::C.extensions(), &["c"]);

View File

@@ -15,6 +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/>.
/// Core build module for DCR — a Cargo-like C/C++ project manager.
/// This module defines the build engine and re-exports key components for
/// building C/C++ projects.
#[cfg(feature = "archive")]
pub mod archive;
pub mod builder;

View File

@@ -21,6 +21,7 @@
use std::sync::{Arc, atomic::AtomicBool};
/// Represents a build request with options for the build process, including profile selection, target specification, force flag, verbosity, workspace, and a cancellation signal.
pub struct BuildRequest {
pub profile: String,
pub target: Option<String>,
@@ -30,21 +31,25 @@ pub struct BuildRequest {
pub cancel: Arc<AtomicBool>,
}
/// Contains the result of a build operation, specifically the duration in seconds.
#[allow(dead_code)]
pub struct BuildOutcome {
pub secs: f64,
}
/// Wraps a string message as a build error.
pub struct BuildError {
pub message: String,
}
/// Creates a BuildError from a String message.
impl From<String> for BuildError {
fn from(message: String) -> Self {
BuildError { message }
}
}
/// Represents events that the build engine can emit during compilation and packaging.
pub enum BuildEvent<'a> {
TargetStart {
index: usize,
@@ -81,6 +86,8 @@ pub enum BuildEvent<'a> {
},
}
/// Trait for handling build events in different reporting backends.
pub trait BuildReporter {
/// Callback invoked when a build event is generated.
fn on_event(&mut self, event: BuildEvent<'_>);
}

View File

@@ -18,6 +18,12 @@
use crate::core::build_config::Config;
use crate::utils::build::{get_config_str, normalize_target_os, profile_table};
/// Retrieves a raw TOML value from the config using a prioritized lookup order based on target, profile, and base sections.
///
/// The keys are checked in this order:
/// {section}.{target}.{profile}.{field}, {section}.{profile}.{target}.{field}, {section}.{target}.{field}, {section}.{profile}.{field}, {section}.{field}
///
/// Returns the value if found, otherwise None.
pub(crate) fn get_config_value_raw(
config: &Config,
section: &str,
@@ -57,6 +63,9 @@ pub(crate) fn get_config_value_raw(
None
}
/// Checks if the 'inherit' flag is enabled for the given section, profile, and target.
///
/// Returns true by default if the flag is not set.
pub(crate) fn get_inherit(
config: &Config,
section: &str,
@@ -68,6 +77,9 @@ pub(crate) fn get_inherit(
.unwrap_or(true)
}
/// Extracts a trimmed non-empty string value from the config.
///
/// Returns Some(value) if found, None otherwise.
pub(crate) fn get_config_value(
config: &Config,
section: &str,
@@ -80,6 +92,9 @@ pub(crate) fn get_config_value(
.filter(|s| !s.is_empty())
}
/// Fetches a string value from the build section, inheriting from base if enabled.
///
/// Returns the value or default empty string if not inheriting.
pub(crate) fn get_string_with_profile_and_target(
config: &Config,
field: &str,
@@ -94,10 +109,12 @@ pub(crate) fn get_string_with_profile_and_target(
}
}
/// Gets a build string value for the given profile without a target.
pub fn get_build_string_with_profile(config: &Config, field: &str, profile: &str) -> String {
get_string_with_profile_and_target(config, field, profile, None)
}
/// Gets a string value from the language-specific build.{lang} section.
pub(crate) fn get_lang_string(
config: &Config,
lang: &str,
@@ -108,6 +125,7 @@ pub(crate) fn get_lang_string(
get_config_value(config, &format!("build.{lang}"), field, profile, target)
}
/// Gets a list of strings from the language-specific build.{lang} section.
pub(crate) fn get_lang_list(
config: &Config,
lang: &str,
@@ -121,6 +139,7 @@ pub(crate) fn get_lang_list(
}
}
/// Parses a TOML array into a Vec<String>, erroring if not an array of strings.
pub(crate) fn parse_string_array(value: &toml::Value, key: &str) -> Result<Vec<String>, String> {
let arr = value
.as_array()
@@ -135,6 +154,9 @@ pub(crate) fn parse_string_array(value: &toml::Value, key: &str) -> Result<Vec<S
Ok(out)
}
/// Collects a list of strings from config, supporting inheritance from base and profile/target overrides.
///
/// If inherit is true, starts with the base list and appends custom entries if defined.
pub(crate) fn get_list_with_profile_and_target(
config: &Config,
field: &str,
@@ -154,6 +176,7 @@ pub(crate) fn get_list_with_profile_and_target(
.get(&format!("build.{field}"))
.map(|v| v == &val)
.unwrap_or(false);
// Skip re-adding the base list when this is the base definition to prevent duplication
if !is_base {
let custom = parse_string_array(&val, &format!("build.{field}"))?;
if inherit {
@@ -184,6 +207,7 @@ pub(crate) fn get_list_with_profile_and_target(
Ok(out)
}
/// Gets the list of strings for the given profile without a target.
pub(crate) fn get_list_with_profile(
config: &Config,
field: &str,
@@ -192,10 +216,21 @@ pub(crate) fn get_list_with_profile(
get_list_with_profile_and_target(config, field, profile, None)
}
/// Returns the list of targets for the given profile.
pub(crate) fn get_targets(config: &Config, profile: &str) -> Result<Vec<String>, String> {
get_list_with_profile(config, "targets", profile)
}
/// Parses the config value as a list of strings.
///
/// # Parameters
/// - `config`: Project config.
/// - `key`: Dot-path key (e.g. `build.sources`).
///
/// # Returns
/// - `Ok([])` if the key is missing.
/// - `Ok(vec)` for an array of strings.
/// - `Err` if present but not an array of strings.
pub(crate) fn get_config_list(config: &Config, key: &str) -> Result<Vec<String>, String> {
let value = match config.get(key) {
Some(v) => v,

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;
@@ -30,6 +29,7 @@ pub(crate) struct BuildStep {
cmd: String,
}
/// Holds interpolated version and profile strings for build step substitution.
pub(crate) struct StepVars<'a> {
pub(crate) profile: &'a str,
pub(crate) version: &'a str,
@@ -40,6 +40,7 @@ pub(crate) struct StepVars<'a> {
pub(crate) version_suffix_dash: &'a str,
}
/// Parses TOML array of build step tables into Vec<BuildStep>.
fn get_build_steps_from_value(value: &toml::Value, key: &str) -> Result<Vec<BuildStep>, String> {
let arr = value
.as_array()
@@ -86,6 +87,7 @@ fn get_build_steps_from_value(value: &toml::Value, key: &str) -> Result<Vec<Buil
Ok(out)
}
/// Retrieves build steps for a specific profile from config, falling back to default if not present.
pub(crate) fn get_build_steps_with_profile(
config: &Config,
field: &str,
@@ -99,6 +101,7 @@ pub(crate) fn get_build_steps_with_profile(
get_build_steps(config, &format!("build.{field}"))
}
/// Gets build steps from config for the given key.
fn get_build_steps(config: &Config, key: &str) -> Result<Vec<BuildStep>, String> {
let value = match config.get(key) {
Some(v) => v,
@@ -110,6 +113,7 @@ fn get_build_steps(config: &Config, key: &str) -> Result<Vec<BuildStep>, String>
use crate::core::build::report::{BuildEvent, BuildReporter};
use std::sync::{Arc, atomic::AtomicBool};
/// Runs all build steps in sequence, checking for cancellation and reporting progress.
pub(crate) fn run_build_steps(
steps: &[BuildStep],
tools: &ToolchainExecs,
@@ -127,6 +131,7 @@ pub(crate) fn run_build_steps(
Ok(())
}
/// Executes a single build step, handling glob expansion, output paths, and command substitution.
fn run_build_step(
step: &BuildStep,
tools: &ToolchainExecs,
@@ -137,6 +142,7 @@ fn run_build_step(
) -> Result<(), String> {
let input_pattern = expand_step_value(&step.input, "", vars);
let inputs = expand_glob(&input_pattern)?;
// Skip step if no matching input files found
if inputs.is_empty() {
return Ok(());
}
@@ -170,6 +176,7 @@ fn run_build_step(
Ok(())
}
/// Deletes files (not directories) matching the given glob patterns.
pub(crate) fn clean_generated_files(patterns: &[String]) -> Result<(), String> {
for pattern in patterns {
for path in expand_glob(pattern)? {
@@ -181,6 +188,7 @@ pub(crate) fn clean_generated_files(patterns: &[String]) -> Result<(), String> {
Ok(())
}
/// Expands glob patterns to find matching files.
pub(crate) fn expand_glob(pattern: &str) -> Result<Vec<PathBuf>, String> {
let mut out = Vec::new();
let entries = glob(pattern).map_err(|err| format!("glob error: {err}"))?;
@@ -191,6 +199,7 @@ pub(crate) fn expand_glob(pattern: &str) -> Result<Vec<PathBuf>, String> {
Ok(out)
}
/// Verifies that expected artifacts are present by expanding patterns.
pub(crate) fn verify_expectations(patterns: &[String], vars: &StepVars) -> Result<(), String> {
for pattern in patterns {
let expanded = expand_step_value(pattern, "", vars);
@@ -202,6 +211,7 @@ pub(crate) fn verify_expectations(patterns: &[String], vars: &StepVars) -> Resul
Ok(())
}
/// True if the step should run: missing/unreadable output, unreadable input, or input newer than output.
fn should_run_step(input: &Path, output: &Path) -> bool {
let in_time = fs::metadata(input).and_then(|m| m.modified());
let out_time = fs::metadata(output).and_then(|m| m.modified());
@@ -212,6 +222,7 @@ fn should_run_step(input: &Path, output: &Path) -> bool {
}
}
/// Substitutes variables into step command template and replaces placeholders.
fn substitute_step_cmd(
template: &str,
input: &Path,
@@ -232,6 +243,7 @@ fn substitute_step_cmd(
.replace("{stem}", stem)
}
/// Builds a list of compiler flags from cflags, include dirs, and compiler type.
pub(crate) fn build_step_flags(
cflags: &[String],
include_dirs: &[String],
@@ -264,6 +276,7 @@ pub(crate) fn build_step_flags(
.join(" ")
}
/// Quotes argument if it contains whitespace or double quotes.
fn quote_step_arg(arg: String) -> String {
if !arg.chars().any(|c| c.is_whitespace() || c == '"') {
return arg;
@@ -272,6 +285,7 @@ fn quote_step_arg(arg: String) -> String {
format!("\"{escaped}\"")
}
/// Checks if compiler is MSVC based on name.
fn is_msvc_compiler(compiler: &str) -> bool {
let lower = compiler.to_lowercase();
lower.contains("cl.exe")
@@ -280,6 +294,7 @@ fn is_msvc_compiler(compiler: &str) -> bool {
|| lower.contains("msvc")
}
/// Runs shell command with output capture and cancellation support.
fn run_shell_command(
cmd: &str,
cancel: &Arc<AtomicBool>,
@@ -352,6 +367,7 @@ fn run_shell_command(
Ok(status)
}
/// Checks if any build step needs to be run by comparing timestamps.
pub(crate) fn build_steps_need_run(steps: &[BuildStep], vars: &StepVars) -> Result<bool, String> {
for step in steps {
let input_pattern = expand_step_value(&step.input, "", vars);
@@ -380,6 +396,7 @@ pub(crate) fn build_steps_need_run(steps: &[BuildStep], vars: &StepVars) -> Resu
Ok(false)
}
/// Creates VersionInfo from StepVars.
fn make_version_info(vars: &StepVars) -> VersionInfo {
VersionInfo {
full: vars.version.to_string(),
@@ -391,6 +408,7 @@ fn make_version_info(vars: &StepVars) -> VersionInfo {
}
}
/// Expands step value by substituting vars and stem.
fn expand_step_value(template: &str, stem: &str, vars: &StepVars) -> String {
let info = make_version_info(vars);
let s = substitute_vars(template, &info, vars.profile, "");

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";
@@ -39,6 +40,7 @@ const VALID_KINDS: &[&str] = &[
"flat-bin",
];
/// Represents errors that can occur during configuration loading, parsing, or validation.
#[derive(Debug)]
pub enum ConfigError {
Io(std::io::Error),
@@ -80,6 +82,7 @@ impl From<toml::ser::Error> for ConfigError {
}
}
/// Core configuration structure that manages TOML parsing, typed views, and validation for DCR projects.
pub struct Config {
path: PathBuf,
data: Value,
@@ -91,6 +94,7 @@ pub struct Config {
}
#[allow(dead_code)]
/// Top-level typed configuration parsed from TOML, containing optional sections for package, build, dependencies, etc.
#[derive(Debug, Clone, Deserialize)]
pub struct DcrConfig {
#[serde(default)]
@@ -108,6 +112,7 @@ pub struct DcrConfig {
}
#[allow(dead_code)]
/// Configuration for archive output formats (e.g. EFI, bootable images).
#[derive(Debug, Clone, Deserialize)]
pub struct ArchiveConfig {
pub output: String,
@@ -121,16 +126,24 @@ pub struct ArchiveConfig {
}
#[allow(dead_code)]
/// Layout definition for archive sections (from/to paths).
#[derive(Debug, Clone, Deserialize)]
pub struct ArchiveLayout {
pub from: String,
pub to: String,
}
/// Package metadata section (name, version, type, dcr-version).
#[derive(Debug, Clone, Deserialize)]
pub struct PackageConfig {
pub name: String,
pub version: String,
/// Minimum/target DCR tool version this project was authored for (`package.dcr-version`).
///
/// Compared against the running `dcr` binary (`CARGO_PKG_VERSION`) on config load.
/// Missing or empty means "no pin" — no warning is emitted.
#[serde(default, rename = "dcr-version")]
pub dcr_version: Option<String>,
#[serde(default, rename = "type")]
pub pkg_type: Option<String>,
}
@@ -144,6 +157,7 @@ fn default_compiler() -> String {
}
#[allow(dead_code)]
/// Build configuration section, including language, standards, compiler, and output settings.
#[derive(Debug, Clone, Deserialize)]
pub struct BuildConfig {
#[serde(default = "default_language")]
@@ -182,6 +196,7 @@ pub struct BuildConfig {
pub qt: Option<bool>,
}
/// Language configuration can be a single string or array of strings (e.g. for multi-language builds).
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum LanguageConfig {
@@ -198,6 +213,7 @@ impl LanguageConfig {
}
}
/// Dependency configuration can be a version string or a full table of dependency details.
#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
@@ -206,6 +222,7 @@ pub enum DependencyConfig {
Table(BTreeMap<String, Value>),
}
/// Toolchain configuration for custom compilers, assemblers, and linkers.
#[derive(Debug, Clone, Deserialize, Default)]
pub struct ToolchainConfig {
pub cc: Option<String>,
@@ -216,6 +233,7 @@ pub struct ToolchainConfig {
pub ld: Option<String>,
}
/// Workspace member configuration for multi-project workspaces.
#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
pub struct WorkspaceMemberConfig {
@@ -227,12 +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)?;
@@ -246,9 +271,11 @@ impl Config {
doc,
};
cfg.validate()?;
cfg.warn_dcr_version_mismatch();
Ok(cfg)
}
/// Opens an existing Config from a TOML file path, failing if the file does not exist.
pub fn open(path: &str) -> Result<Self, ConfigError> {
let path = PathBuf::from(path);
if !path.exists() {
@@ -263,20 +290,86 @@ impl Config {
doc,
};
cfg.validate()?;
cfg.warn_dcr_version_mismatch();
Ok(cfg)
}
#[allow(dead_code)]
/// Returns the fully typed configuration struct.
pub fn typed(&self) -> &DcrConfig {
&self.typed
}
#[allow(dead_code)]
/// Returns the package configuration if present.
pub fn package(&self) -> Option<&PackageConfig> {
self.typed.package.as_ref()
}
/// Warn once per config path about `package.dcr-version` vs this `dcr` binary.
///
/// - Missing/`""` pin → recommend adding `dcr-version`.
/// - Tool older than pin → project may use features this binary does not support.
/// - Tool newer than pin → project may rely on outdated defaults; consider bumping the pin.
///
/// Never fails the load; warnings only. Equal pin is silent.
fn warn_dcr_version_mismatch(&self) {
let Some(pkg) = self.typed.package.as_ref() else {
return;
};
let path_key = self
.path
.canonicalize()
.unwrap_or_else(|_| self.path.clone())
.to_string_lossy()
.into_owned();
if !dcr_version_warn_once(&path_key) {
return;
}
let tool = env!("CARGO_PKG_VERSION");
let required = pkg
.dcr_version
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty());
let Some(required) = required else {
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(),
);
return;
};
match crate::utils::build::compare_semver(tool, required) {
Some(std::cmp::Ordering::Less) => {
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) => {
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.",
);
}
Some(std::cmp::Ordering::Equal) => {}
None => {
warn!(
"package.dcr-version = \"{required}\" is not a valid semver (expected X.Y.Z); ignoring",
);
}
}
}
#[allow(dead_code)]
/// Returns the build configuration if present.
pub fn build_config(&self) -> Option<&BuildConfig> {
self.typed.build.as_ref()
}
@@ -290,7 +383,7 @@ impl Config {
&& !self.typed.workspace.is_empty()
}
/// Merge build fields from parent config into self where self has empty/default values.
/// Merges build fields from a parent config into self where self has empty/default values.
/// Used for workspace member inheritance (build.inherit = true).
pub fn merge_parent(&mut self, parent: &Config) {
let fields: &[&str] = &[
@@ -368,24 +461,29 @@ impl Config {
}
}
/// Retrieves a nested value from the config data using dot-separated path.
pub fn get(&self, key: &str) -> Option<&Value> {
let parts: Vec<&str> = key.split('.').collect();
get_path(&self.data, &parts)
}
#[allow(dead_code)]
/// Adds a new key-value pair to the configuration (alias for set).
pub fn add(&mut self, key: &str, value: Value) -> Result<(), ConfigError> {
self.set(key, value)
}
/// Updates a configuration value at the given key path.
pub fn edit(&mut self, key: &str, value: Value) -> Result<(), ConfigError> {
self.set(key, value)
}
#[allow(dead_code)]
/// Checks if the configuration is valid without returning the error.
pub fn check(&self) -> bool {
self.validate().is_ok()
}
/// Performs comprehensive validation of the configuration, including package, build, toolchain, and workspace rules.
pub fn validate(&self) -> Result<(), ConfigError> {
if self.is_workspace_only() {
return self.validate_workspace();
@@ -739,16 +837,24 @@ fn load_parts(path: &Path) -> Result<(Value, DocumentMut), ConfigError> {
Ok((data, doc))
}
fn default_toml_text() -> String {
let name = std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string()))
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\
dcr-version = \"{dcr_version}\"\n\
type = \"none\"\n\
description = \"\"\n\
author = \"\"\n\
@@ -761,7 +867,18 @@ fn default_toml_text() -> String {
kind = \"{DEFAULT_KIND}\"\n\
\n\
[dependencies]\n"
)
))
}
fn dcr_version_warn_once(path_key: &str) -> bool {
use std::collections::HashSet;
use std::sync::{Mutex, OnceLock};
static WARNED: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
let set = WARNED.get_or_init(|| Mutex::new(HashSet::new()));
let Ok(mut guard) = set.lock() else {
return true;
};
guard.insert(path_key.to_string())
}
fn set_doc_path(doc: &mut DocumentMut, path: &[&str], value: &Value) -> Result<(), ConfigError> {
@@ -916,11 +1033,21 @@ mod tests {
let dir = temp_dir("typed_config");
let path = write_toml_file(
&dir,
"[package]\nname = \"typed\"\nversion = \"1.2.3\"\ntype = \"lib\"\n\n[build]\nlanguage = [\"c\", \"c++\"]\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"staticlib\"\ncflags = [\"-Wall\"]\n\n[dependencies]\nfoo = \"1.0.0\"\n",
"[package]\nname = \"typed\"\nversion = \"1.2.3\"\ndcr-version = \"0.8.4\"\ntype = \"lib\"\n\n[build]\nlanguage = [\"c\", \"c++\"]\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"staticlib\"\ncflags = [\"-Wall\"]\n\n[dependencies]\nfoo = \"1.0.0\"\n",
);
let config = Config::open(&path.to_string_lossy()).unwrap();
assert_eq!(config.package().unwrap().name, "typed");
assert_eq!(config.typed().package.as_ref().unwrap().version, "1.2.3");
assert_eq!(
config
.typed()
.package
.as_ref()
.unwrap()
.dcr_version
.as_deref(),
Some("0.8.4")
);
assert_eq!(config.build_config().unwrap().compiler, "clang");
assert_eq!(config.build_config().unwrap().cflags, ["-Wall"]);
assert!(config.typed().dependencies.contains_key("foo"));
@@ -1171,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

@@ -1,6 +1,10 @@
/// Represents resolved dependencies for a build target, including include directories, library directories, and libraries.
#[derive(Debug, Clone, Default)]
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,9 +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 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)]
pub struct DepLock {
pub name: String,
@@ -26,6 +26,7 @@ pub struct DepLock {
pub source: String,
}
/// Writes the dcr.lock file in a Cargo-like format. The project entry comes first, followed by dependency entries.
pub fn write_lock(
project_root: &Path,
project_name: &str,
@@ -47,6 +48,7 @@ pub fn write_lock(
}
out.push('\n');
// Write metadata for each package in the dependencies list
for pkg in packages {
out.push_str("[[package]]\n");
out.push_str(&format!("name = \"{}\"\n", escape_value(&pkg.name)));
@@ -60,6 +62,7 @@ pub fn write_lock(
Ok(())
}
/// Converts a list of strings into a comma-separated TOML array by quoting each item.
fn quote_list(items: &[String]) -> String {
items
.iter()
@@ -68,6 +71,7 @@ fn quote_list(items: &[String]) -> String {
.join(", ")
}
/// Escapes backslashes and double quotes in a string to make it safe for TOML.
fn escape_value(input: &str) -> String {
input.replace('\\', "\\\\").replace('"', "\\\"")
}

View File

@@ -15,6 +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/>.
/// Core dependency resolver for DCR projects.
pub mod common;
pub mod lock;
pub mod register;
@@ -22,8 +23,10 @@ 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.
fn dep_version(path: &Path) -> String {
Config::open(&path.join("dcr.toml").to_string_lossy())
.ok()
@@ -35,6 +38,18 @@ fn dep_version(path: &Path) -> String {
.unwrap_or_default()
}
/// Resolves dependencies from config into include/lib dirs and lib names, and writes the lock file.
///
/// 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`.
/// - `_profile` / `_target`: Reserved for future profile/target-specific deps.
/// - `project_root`: Root used to resolve path deps and write `dcr.lock`.
///
/// # Returns
/// Aggregated include/lib/libs, or an error string on resolve/lock failure.
pub fn resolve_deps(
config: &Config,
_profile: &str,
@@ -57,8 +72,24 @@ pub fn resolve_deps(
let mut lock_packages: Vec<DepLock> = Vec::new();
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)?;
@@ -73,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(),
@@ -92,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 {
@@ -103,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()) {
@@ -128,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 {
@@ -147,16 +193,52 @@ 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),
});
}
}
}
// Write the lock file with all resolved dependencies
write_lock(
project_root,
&project_name,
@@ -167,11 +249,16 @@ pub fn resolve_deps(
Ok(resolved)
}
/// Git dependency specification (url + optional version/ref for the lockfile).
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.
fn git_dep(value: &toml::Value) -> Option<GitDep<'_>> {
if let Some(table) = value.as_table()
&& let Some(url) = table.get("git").and_then(|v| v.as_str())
@@ -180,11 +267,72 @@ 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() {
return table.get("path").and_then(|v| v.as_str());
@@ -192,12 +340,14 @@ fn path_dep_path(value: &toml::Value) -> Option<&str> {
register::path_from_string_dep(value)
}
/// Adds a directory path to the list if it exists on the filesystem.
fn push_if_exists(paths: &mut Vec<String>, path: &Path) {
if path.exists() {
paths.push(path.to_string_lossy().to_string());
}
}
/// Adds common library directory candidates to the list if they exist.
fn push_default_lib_dirs(paths: &mut Vec<String>, dep_root: &Path) {
for dir in ["lib", "lib64"] {
push_if_exists(paths, &dep_root.join(dir));
@@ -205,11 +355,123 @@ 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::*;
use toml::Value;
/// Tests that path_dep_path correctly handles both TOML table and legacy string inputs for path dependencies.
#[test]
fn path_dep_path_supports_table_and_legacy_strings() {
let table = Value::Table(
@@ -232,6 +494,21 @@ 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() {
let root = std::env::temp_dir().join(format!(
@@ -242,10 +519,13 @@ mod tests {
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(root.join("target/lib")).unwrap();
std::fs::create_dir_all(root.join("target").join("lib")).unwrap();
let mut paths = Vec::new();
push_default_lib_dirs(&mut paths, &root);
assert!(paths.iter().any(|p| p.ends_with("target/lib")));
assert!(paths.iter().any(|p| {
let n = p.replace('\\', "/");
n.ends_with("target/lib")
}));
let _ = std::fs::remove_dir_all(root);
}
}

View File

@@ -17,29 +17,36 @@
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.
#[derive(Debug, Serialize, Deserialize)]
pub struct Registry {
pub url: String,
pub priority: i32,
}
/// Configuration struct for DCR registries, mapping registry names to Registry details.
#[derive(Debug, Serialize, Deserialize)]
pub struct DcrConfig {
pub registry: std::collections::HashMap<String, Registry>,
}
/// Helper function to get the user's home directory path.
fn home_dir() -> Option<PathBuf> {
crate::utils::fs::home_dir()
}
/// Helper to get the DCR config directory path.
fn dcr_config_dir() -> Option<PathBuf> {
home_dir().map(|h| h.join(".dcr"))
}
/// Loads DCR registry configuration from the TOML file in the user's home directory.
pub fn get_registry_config() -> Option<DcrConfig> {
let home = home_dir()?;
let config_path = home.join(".dcr/config.toml");
@@ -51,6 +58,7 @@ pub fn get_registry_config() -> Option<DcrConfig> {
toml::from_str(&content).ok()
}
/// Returns the path to the registry index file, preferring the DCR_INDEX_PATH env var or falling back to ~/.dcr/index.json.
pub fn get_index_path() -> PathBuf {
std::env::var("DCR_INDEX_PATH")
.map(PathBuf::from)
@@ -61,6 +69,7 @@ pub fn get_index_path() -> PathBuf {
})
}
/// Returns the root directory for registry cache, based on the index path's parent, creating it if necessary.
pub fn get_registry_cache_root() -> PathBuf {
let index_path = get_index_path();
if let Some(parent) = index_path.parent() {
@@ -76,6 +85,7 @@ pub fn get_registry_cache_root() -> PathBuf {
}
}
/// Extracts the package root path from registry info. If the path points to a dcr.toml manifest, returns its parent directory.
pub fn package_root_from_registry_info(pkg_info: &JsonValue) -> Result<PathBuf, String> {
let raw_path = pkg_info
.get("path")
@@ -85,12 +95,7 @@ pub fn package_root_from_registry_info(pkg_info: &JsonValue) -> Result<PathBuf,
return Err("Registry package path is empty".to_string());
}
let path = Path::new(raw_path);
let full_path = if path.is_absolute() {
path.to_path_buf()
} else {
get_registry_cache_root().join(path)
};
let full_path = registry_path_to_pathbuf(raw_path.trim());
if full_path.file_name().and_then(|v| v.to_str()) == Some("dcr.toml") {
return full_path
@@ -102,14 +107,149 @@ pub fn package_root_from_registry_info(pkg_info: &JsonValue) -> Result<PathBuf,
Ok(full_path)
}
/// Converts a raw registry path string to a PathBuf, handling file:// prefixes, Windows extended paths, and relative paths by joining to cache root.
pub fn registry_path_to_pathbuf(raw: &str) -> PathBuf {
let s = raw.trim();
if let Some(rest) = s.strip_prefix("file://") {
if rest.len() >= 3 && rest.as_bytes()[0] == b'/' && rest.as_bytes()[2] == b':' {
return PathBuf::from(&rest[1..]);
}
if rest.len() >= 2 && rest.as_bytes()[1] == b':' {
return PathBuf::from(rest);
}
if rest.starts_with('/') {
return PathBuf::from(rest);
}
return PathBuf::from(format!("/{rest}"));
}
let mut s = s.replace('\\', "/");
if let Some(rest) = s.strip_prefix("//?/") {
s = rest.to_string();
} else if let Some(rest) = s.strip_prefix("//./") {
s = rest.to_string();
}
if s.len() >= 3 && s.as_bytes()[0] == b'/' && s.as_bytes()[2] == b':' {
s = s[1..].to_string();
}
let path = PathBuf::from(&s);
if path.is_absolute() || (s.len() >= 2 && s.as_bytes()[1] == b':') || s.starts_with('/') {
path
} else {
get_registry_cache_root().join(path)
}
}
/// Returns the include directory path for a dependency root, typically under target/include.
pub fn registry_include_dir(dep_root: &Path) -> PathBuf {
dep_root.join("target").join("include")
}
/// Returns the library directory path for a dependency root, typically under target/lib.
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
/// same local index path (per-registry URLs are unused).
///
/// # Parameters
/// - `name`: Package name as in the index.
///
/// # Returns
/// Package JSON object, or an error if config/index is missing or the name is absent.
pub fn resolve_package_from_registry(name: &str) -> Result<JsonValue, String> {
let config = get_registry_config().ok_or("No registry config found")?;
let mut registries: Vec<(&String, &Registry)> = config.registry.iter().collect();
@@ -137,6 +277,10 @@ pub fn resolve_package_from_registry(name: &str) -> Result<JsonValue, String> {
))
}
/// True if the TOML value is a registry-style dependency.
///
/// Strings: not path-like and not git-like. Tables: have version/features/optional/registry
/// and no `git`/`path`/`url` keys.
pub fn is_registry_dep(value: &TomlValue) -> bool {
if let Some(raw) = value.as_str() {
let raw = raw.trim();
@@ -155,6 +299,7 @@ pub fn is_registry_dep(value: &TomlValue) -> bool {
}
}
/// Extracts the path string from a dependency value if it's a path-like dependency.
pub fn path_from_string_dep(value: &TomlValue) -> Option<&str> {
let raw = value.as_str()?.trim();
if let Some(path) = raw.strip_prefix("path:") {
@@ -166,6 +311,7 @@ pub fn path_from_string_dep(value: &TomlValue) -> Option<&str> {
None
}
/// Helper to check if a string represents a local file path.
fn is_path_like_string(raw: &str) -> bool {
raw.starts_with("path:")
|| raw.starts_with("./")
@@ -175,6 +321,7 @@ fn is_path_like_string(raw: &str) -> bool {
|| raw.contains('\\')
}
/// Helper to check if a string represents a git or remote dependency.
fn is_git_like_string(raw: &str) -> bool {
raw.starts_with("git:")
|| raw.starts_with("github:")
@@ -219,16 +366,44 @@ mod tests {
#[test]
fn registry_package_root_accepts_package_dir_or_manifest_path() {
let pkg = serde_json::json!({ "path": "/tmp/pkg" });
let root = std::env::temp_dir().join(format!("dcr_reg_pkg_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).unwrap();
let root_str = root.to_string_lossy().replace('\\', "/");
let pkg = serde_json::json!({ "path": root_str });
assert_eq!(
package_root_from_registry_info(&pkg).unwrap(),
PathBuf::from("/tmp/pkg")
package_root_from_registry_info(&pkg)
.unwrap()
.to_string_lossy()
.replace('\\', "/"),
root_str
);
let pkg = serde_json::json!({ "path": "/tmp/pkg/dcr.toml" });
let manifest = format!("{root_str}/dcr.toml");
let pkg = serde_json::json!({ "path": manifest });
assert_eq!(
package_root_from_registry_info(&pkg).unwrap(),
PathBuf::from("/tmp/pkg")
package_root_from_registry_info(&pkg)
.unwrap()
.to_string_lossy()
.replace('\\', "/"),
root_str
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn registry_path_strips_windows_extended_prefix() {
let p = registry_path_to_pathbuf("//?/D:/cache/mylib");
let s = p.to_string_lossy().replace('\\', "/");
assert_eq!(s, "D:/cache/mylib");
let p2 = registry_path_to_pathbuf("/D:/cache/mylib");
let s2 = p2.to_string_lossy().replace('\\', "/");
assert_eq!(s2, "D:/cache/mylib");
let p3 = registry_path_to_pathbuf("file:///D:/cache/mylib");
let s3 = p3.to_string_lossy().replace('\\', "/");
assert_eq!(s3, "D:/cache/mylib");
}
}

View File

@@ -15,6 +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/>.
/// Core module for DCR, a Cargo-like C/C++ project manager.
pub mod build;
pub mod build_config;
pub mod deps;

View File

@@ -15,33 +15,38 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
/// 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)]
pub struct Registry {
pub url: String,
pub priority: i32,
}
/// Represents the overall configuration loaded from config.toml, mapping registry names to their details.
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
pub registry: HashMap<String, Registry>,
}
/// Manages the DCR registry configuration, loading from ~/.dcr/config.toml.
pub struct RegistryManager {
pub config: Config,
#[allow(dead_code)]
pub path: PathBuf,
}
/// Returns the user's home directory path, used to locate the config file.
fn home_dir() -> Option<PathBuf> {
crate::utils::fs::home_dir()
}
impl RegistryManager {
/// Loads the registry configuration from ~/.dcr/config.toml.
/// Returns an error if the config file is missing.
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
let dcr_dir = home_dir()
.ok_or("Cannot determine home directory")?
@@ -53,6 +58,7 @@ impl RegistryManager {
}
let content = fs::read_to_string(&config_path)?;
// Deserialize the TOML content into the Config struct
let config: Config = toml::from_str(&content)?;
Ok(Self {

View File

@@ -16,13 +16,28 @@
// 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};
pub fn run_binary(project_name: &str, profile: &str, target_dir: Option<&str>) -> i32 {
/// Runs the built binary for a project, forwarding `bin_args`.
///
/// # Parameters
/// - `project_name`: Package/binary name used to resolve the artifact path.
/// - `profile`: Build profile segment under `target/`.
/// - `target_dir`: Optional artifact directory override.
/// - `bin_args`: Arguments passed to the process.
///
/// # Returns
/// Child exit code, or `1` if the binary is missing or cannot be spawned.
pub fn run_binary(
project_name: &str,
profile: &str,
target_dir: Option<&str>,
bin_args: &[String],
) -> i32 {
let bin_path = platform::bin_path(profile, project_name, target_dir);
// Check if the binary exists before attempting to run it
if Path::new(&bin_path).exists() {
let status = Command::new(&bin_path).status();
let status = Command::new(&bin_path).args(bin_args).status();
match status {
Ok(s) => {
return s.code().unwrap_or(0);
@@ -32,5 +47,6 @@ pub fn run_binary(project_name: &str, profile: &str, target_dir: Option<&str>) -
}
}
}
// Return 1 if binary not found or execution failed
1
}

View File

@@ -15,17 +15,23 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::fs;
use std::path::Path;
use std::{fs, path::Path};
// creating git init and .gitignore
/// Initializes a Git repository in the target directory and configures its `.gitignore` file.
///
/// Executes `git init` via the VCS utility module, creates a default `.gitignore` if none exists,
/// or updates an existing `.gitignore` to ensure `.dcr/` and build outputs are ignored.
pub fn init(path: &Path) -> Result<(), String> {
crate::utils::git::git_init(path)?;
// Populate default .gitignore entries or append DCR metadata rules.
let gitignore_path = path.join(".gitignore");
if !gitignore_path.exists() {
fs::write(&gitignore_path, "/target\n")
fs::write(&gitignore_path, "/target\n.dcr/\n")
.map_err(|e| format!("failed to create .gitignore: {}", e))?;
} else {
crate::utils::fs::ensure_gitignore_has_dcr(path)
.map_err(|e| format!("failed to update .gitignore: {}", e))?;
}
Ok(())

View File

@@ -15,10 +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/>.
/// Module for version control system (VCS) integration.
pub mod git;
use std::path::Path;
/// Represents the version control system kind to use.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VcsKind {
Git,
@@ -26,6 +28,7 @@ pub enum VcsKind {
}
impl VcsKind {
/// Parses the given string into a VcsKind.
pub fn parse(s: &str) -> Result<Self, String> {
match s.to_lowercase().as_str() {
"git" => Ok(Self::Git),
@@ -35,6 +38,7 @@ impl VcsKind {
}
}
/// Initializes the VCS for the provided path.
pub fn init_vcs(kind: VcsKind, path: &Path) -> Result<(), String> {
match kind {
VcsKind::Git => git::init(path),
@@ -42,8 +46,9 @@ pub fn init_vcs(kind: VcsKind, path: &Path) -> Result<(), String> {
}
}
// look for git repo in parents to avoid nested repos
/// Finds an existing VCS in the parent directories of the given path.
pub fn find_existing_vcs(path: &Path) -> Option<VcsKind> {
// look for git repo in parents to avoid nested repos
let mut current = if path.is_file() {
path.parent()
} else {

View File

@@ -21,6 +21,7 @@ use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use toml::Value;
/// Represents a member of the workspace.
#[derive(Debug, Clone)]
pub struct WorkspaceMember {
pub name: String,
@@ -29,12 +30,14 @@ pub struct WorkspaceMember {
pub main: bool,
}
/// Represents the workspace as a collection of members.
#[derive(Debug, Clone)]
pub struct Workspace {
pub members: Vec<WorkspaceMember>,
}
impl Workspace {
/// Returns the main workspace member, preferring the one with `main: true` or the first member.
pub fn main_member(&self) -> Option<&WorkspaceMember> {
self.members
.iter()
@@ -43,6 +46,17 @@ impl Workspace {
}
}
/// Parses the workspace configuration from the TOML config.
///
/// # Parameters
/// - `config`: Root `dcr.toml` (may be workspace-only or mixed).
/// - `profile` / `target`: Select profile- and target-specific `[workspace.*]` tables.
/// - `root`: Workspace root used to resolve member paths.
///
/// # Returns
/// - `Ok(None)` if no workspace table applies.
/// - `Ok(Some(Workspace))` with members (and optional main).
/// - `Err` on invalid member paths or config shape.
pub fn parse_workspace(
config: &Config,
profile: &str,
@@ -50,6 +64,7 @@ pub fn parse_workspace(
root: &Path,
) -> Result<Option<Workspace>, String> {
let mut table = None;
// Create ordered list of possible config keys for lookup
let combinations = if let Some(t) = target {
let normalized_t = normalize_target_os(t);
vec![
@@ -103,6 +118,7 @@ pub fn parse_workspace(
Ok(Some(ws))
}
/// Resolves a path, making it absolute if relative to root.
fn resolve_path(root: &Path, raw: &str) -> Result<PathBuf, String> {
let p = Path::new(raw);
let full = if p.is_absolute() {
@@ -113,6 +129,7 @@ fn resolve_path(root: &Path, raw: &str) -> Result<PathBuf, String> {
Ok(full)
}
/// Parses dependencies list from TOML value into Vec<String>.
fn parse_deps(value: Option<&Value>) -> Result<Vec<String>, String> {
let Some(value) = value else {
return Ok(Vec::new());
@@ -130,12 +147,14 @@ fn parse_deps(value: Option<&Value>) -> Result<Vec<String>, String> {
Ok(out)
}
/// Performs a topological sort on members to handle dependencies and detect cycles.
fn topo_sort(members: &[WorkspaceMember]) -> Result<Vec<WorkspaceMember>, String> {
let mut map = HashMap::new();
for m in members {
map.insert(m.name.clone(), m.clone());
}
let mut state: HashMap<String, u8> = HashMap::new();
// Use 0=unvisited, 1=visiting, 2=visited for cycle detection
let mut order: Vec<WorkspaceMember> = Vec::new();
for name in map.keys() {
@@ -146,12 +165,14 @@ fn topo_sort(members: &[WorkspaceMember]) -> Result<Vec<WorkspaceMember>, String
Ok(order)
}
/// Recursive function for DFS-based topological sort with cycle detection.
fn visit(
name: &str,
map: &HashMap<String, WorkspaceMember>,
state: &mut HashMap<String, u8>,
order: &mut Vec<WorkspaceMember>,
) -> Result<(), String> {
// Check current visit state to detect cycles
match state.get(name).copied().unwrap_or(0) {
1 => return Err(format!("workspace dependency cycle at {name}")),
2 => return Ok(()),

View File

@@ -14,15 +14,29 @@
//
// 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() {
let args: Vec<String> = std::env::args().collect();
let args = match utils::output::configure(std::env::args().collect()) {
Ok(args) => args,
Err(message) => {
utils::output::emit("error", &message);
std::process::exit(2);
}
};
utils::log::init();
if args.len() < 2 {
std::process::exit(cli::help::help());
@@ -56,7 +70,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
}
};

View File

@@ -15,63 +15,51 @@
// 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 super::path_util::{default_bin_rel, default_lib_rel, join_dir};
use crate::utils::build::default_target_triple;
/// Returns the host target triple used for default artifact paths on BSD.
fn host_triple() -> String {
default_target_triple()
}
/// Returns the path to a binary for the given profile, name, and optional target directory.
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
None => {
let arch = std::env::consts::ARCH;
let os = std::env::consts::OS;
let target = format!("{arch}-unknown-{os}");
format!("./target/{target}/{profile}/{name}")
}
Some(dir) => join_dir(dir, name),
None => default_bin_rel(profile, name, Some(&host_triple())),
}
}
/// Returns the path to a static library for the given profile, name, and optional target directory.
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
let file = format!("lib{name}.a");
match target_dir {
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
None => {
let arch = std::env::consts::ARCH;
let os = std::env::consts::OS;
let target = format!("{arch}-unknown-{os}");
format!("./target/{target}/{profile}/lib{name}.a")
}
Some(dir) => join_dir(dir, &file),
None => default_lib_rel(profile, &file, Some(&host_triple())),
}
}
/// Returns the path to an ELF binary for the given profile, name, and optional target directory.
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
None => {
let arch = std::env::consts::ARCH;
let os = std::env::consts::OS;
let target = format!("{arch}-unknown-{os}");
format!("./target/{target}/{profile}/{name}")
}
}
bin_path(profile, name, target_dir)
}
/// Returns the path to an EFI binary for the given profile, name, and optional target directory.
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
let file = format!("{name}.efi");
match target_dir {
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
None => {
let arch = std::env::consts::ARCH;
let os = std::env::consts::OS;
let target = format!("{arch}-unknown-{os}");
format!("./target/{target}/{profile}/{name}.efi")
}
Some(dir) => join_dir(dir, &file),
None => default_lib_rel(profile, &file, Some(&host_triple())),
}
}
/// Returns the path to a shared library for the given profile, name, and optional target directory.
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
let file = format!("lib{name}.so");
match target_dir {
Some(dir) => format!("{}/lib{}.so", dir.trim_end_matches('/'), name),
None => {
let arch = std::env::consts::ARCH;
let os = std::env::consts::OS;
let target = format!("{arch}-unknown-{os}");
format!("./target/{target}/{profile}/lib{name}.so")
}
Some(dir) => join_dir(dir, &file),
None => default_lib_rel(profile, &file, Some(&host_triple())),
}
}
@@ -81,25 +69,19 @@ mod tests {
#[test]
fn bin_path_default() {
let arch = std::env::consts::ARCH;
let os = std::env::consts::OS;
let expected = format!("./target/{arch}-unknown-{os}/debug/hello");
let expected = default_bin_rel("debug", "hello", Some(&host_triple()));
assert_eq!(bin_path("debug", "hello", None), expected);
}
#[test]
fn lib_path_default() {
let arch = std::env::consts::ARCH;
let os = std::env::consts::OS;
let expected = format!("./target/{arch}-unknown-{os}/debug/libmylib.a");
let expected = default_lib_rel("debug", "libmylib.a", Some(&host_triple()));
assert_eq!(lib_path("debug", "mylib", None), expected);
}
#[test]
fn shared_lib_path_default() {
let arch = std::env::consts::ARCH;
let os = std::env::consts::OS;
let expected = format!("./target/{arch}-unknown-{os}/debug/libmylib.so");
let expected = default_lib_rel("debug", "libmylib.so", Some(&host_triple()));
assert_eq!(shared_lib_path("debug", "mylib", None), expected);
}
}

View File

@@ -15,58 +15,52 @@
// 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 super::path_util::{default_bin_rel, default_lib_rel, join_dir};
use crate::utils::build::default_target_triple;
/// Returns the default target triple string for the host platform.
fn host_triple() -> String {
default_target_triple()
}
/// Constructs the path to a binary executable.
/// If `target_dir` is provided, joins it with the name; otherwise uses default_bin_rel with the host triple.
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
None => {
let arch = std::env::consts::ARCH;
let target = format!("{arch}-unknown-linux-gnu");
format!("./target/{target}/{profile}/{name}")
}
Some(dir) => join_dir(dir, name),
None => default_bin_rel(profile, name, Some(&host_triple())),
}
}
/// Constructs the path to a static library archive.
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
let file = format!("lib{name}.a");
match target_dir {
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
None => {
let arch = std::env::consts::ARCH;
let target = format!("{arch}-unknown-linux-gnu");
format!("./target/{target}/{profile}/lib{name}.a")
}
Some(dir) => join_dir(dir, &file),
None => default_lib_rel(profile, &file, Some(&host_triple())),
}
}
/// Constructs the path to an ELF binary.
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
None => {
let arch = std::env::consts::ARCH;
let target = format!("{arch}-unknown-linux-gnu");
format!("./target/{target}/{profile}/{name}")
}
}
bin_path(profile, name, target_dir)
}
/// Constructs the path to an EFI binary.
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
let file = format!("{name}.efi");
match target_dir {
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
None => {
let arch = std::env::consts::ARCH;
let target = format!("{arch}-unknown-linux-gnu");
format!("./target/{target}/{profile}/{name}.efi")
}
Some(dir) => join_dir(dir, &file),
None => default_lib_rel(profile, &file, Some(&host_triple())),
}
}
/// Constructs the path to a shared library.
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
let file = format!("lib{name}.so");
match target_dir {
Some(dir) => format!("{}/lib{}.so", dir.trim_end_matches('/'), name),
None => {
let arch = std::env::consts::ARCH;
let target = format!("{arch}-unknown-linux-gnu");
format!("./target/{target}/{profile}/lib{name}.so")
}
Some(dir) => join_dir(dir, &file),
None => default_lib_rel(profile, &file, Some(&host_triple())),
}
}
@@ -76,44 +70,49 @@ mod tests {
#[test]
fn bin_path_default() {
let arch = std::env::consts::ARCH;
let expected = format!("./target/{arch}-unknown-linux-gnu/debug/hello");
let expected = default_bin_rel("debug", "hello", Some(&host_triple()));
assert_eq!(bin_path("debug", "hello", None), expected);
}
#[test]
fn bin_path_release() {
let arch = std::env::consts::ARCH;
let expected = format!("./target/{arch}-unknown-linux-gnu/release/hello");
let expected = default_bin_rel("release", "hello", Some(&host_triple()));
assert_eq!(bin_path("release", "hello", None), expected);
}
#[test]
fn bin_path_custom_target() {
assert_eq!(bin_path("debug", "hello", Some("out")), "out/hello");
assert_eq!(
bin_path("debug", "hello", Some("out")),
join_dir("out", "hello")
);
}
#[test]
fn bin_path_custom_target_trailing_slash() {
assert_eq!(bin_path("debug", "hello", Some("out/")), "out/hello");
assert_eq!(
bin_path("debug", "hello", Some("out/")),
join_dir("out/", "hello")
);
}
#[test]
fn lib_path_default() {
let arch = std::env::consts::ARCH;
let expected = format!("./target/{arch}-unknown-linux-gnu/debug/libmylib.a");
let expected = default_lib_rel("debug", "libmylib.a", Some(&host_triple()));
assert_eq!(lib_path("debug", "mylib", None), expected);
}
#[test]
fn lib_path_custom_target() {
assert_eq!(lib_path("debug", "mylib", Some("out")), "out/libmylib.a");
assert_eq!(
lib_path("debug", "mylib", Some("out")),
join_dir("out", "libmylib.a")
);
}
#[test]
fn shared_lib_path_default() {
let arch = std::env::consts::ARCH;
let expected = format!("./target/{arch}-unknown-linux-gnu/debug/libmylib.so");
let expected = default_lib_rel("debug", "libmylib.so", Some(&host_triple()));
assert_eq!(shared_lib_path("debug", "mylib", None), expected);
}
@@ -121,7 +120,7 @@ mod tests {
fn shared_lib_path_custom_target() {
assert_eq!(
shared_lib_path("release", "mylib", Some("dist")),
"dist/libmylib.so"
join_dir("dist", "libmylib.so")
);
}
}

View File

@@ -15,37 +15,48 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
/// macOS-specific utilities for constructing paths to build artifacts.
///
/// This module defines functions to generate paths for binaries, static
/// libraries, shared libraries, ELF files, and EFI executables on macOS.
use super::path_util::{default_bin_rel, default_lib_rel, join_dir};
/// Constructs the path to a binary for the given profile, name, and optional target directory.
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}"),
Some(dir) => join_dir(dir, name),
None => default_bin_rel(profile, name, None),
}
}
/// Constructs the path to a static library archive for the given profile, name, and optional target directory.
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
let file = format!("lib{name}.a");
match target_dir {
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/lib{name}.a"),
Some(dir) => join_dir(dir, &file),
None => default_lib_rel(profile, &file, None),
}
}
/// Constructs the path to an ELF binary for the given profile, name, and optional target directory.
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}"),
}
bin_path(profile, name, target_dir)
}
/// Constructs the path to an EFI executable for the given profile, name, and optional target directory.
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
let file = format!("{name}.efi");
match target_dir {
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}.efi"),
Some(dir) => join_dir(dir, &file),
None => default_lib_rel(profile, &file, None),
}
}
/// Constructs the path to a shared library for the given profile, name, and optional target directory.
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
let file = format!("lib{name}.dylib");
match target_dir {
Some(dir) => format!("{}/lib{}.dylib", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/lib{name}.dylib"),
Some(dir) => join_dir(dir, &file),
None => default_lib_rel(profile, &file, None),
}
}

View File

@@ -26,6 +26,7 @@ pub mod bsd;
pub mod linux;
#[cfg(target_os = "macos")]
pub mod macos;
mod path_util;
#[cfg(not(any(
target_os = "linux",
target_os = "macos",
@@ -39,6 +40,17 @@ pub mod posix;
#[cfg(target_os = "windows")]
pub mod windows;
/// Host OS path for a binary artifact.
///
/// Backend is chosen at compile time via `cfg!(target_os = …)`, not by the build target triple.
///
/// # Parameters
/// - `profile`: Build profile directory segment.
/// - `name`: Binary basename (may already include extension on some hosts).
/// - `target_dir`: Optional absolute/relative artifact directory override.
///
/// # Returns
/// Path string suitable for the current host layout.
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
#[cfg(target_os = "linux")]
{
@@ -46,11 +58,11 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
}
#[cfg(target_os = "macos")]
{
return macos::bin_path(profile, name, target_dir);
macos::bin_path(profile, name, target_dir)
}
#[cfg(target_os = "windows")]
{
return windows::bin_path(profile, name, target_dir);
windows::bin_path(profile, name, target_dir)
}
#[cfg(any(
target_os = "freebsd",
@@ -59,7 +71,7 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
))]
{
return bsd::bin_path(profile, name, target_dir);
bsd::bin_path(profile, name, target_dir)
}
#[cfg(not(any(
target_os = "linux",
@@ -71,10 +83,13 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
)))]
{
return posix::bin_path(profile, name, target_dir);
posix::bin_path(profile, name, target_dir)
}
}
/// Host OS path for an ELF-style artifact (`profile`, `name`, optional `target_dir`).
///
/// Backend is chosen at compile time via `cfg!(target_os = …)`, not by the build target triple.
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
#[cfg(target_os = "linux")]
{
@@ -82,11 +97,11 @@ pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
}
#[cfg(target_os = "macos")]
{
return macos::elf_path(profile, name, target_dir);
macos::elf_path(profile, name, target_dir)
}
#[cfg(target_os = "windows")]
{
return windows::elf_path(profile, name, target_dir);
windows::elf_path(profile, name, target_dir)
}
#[cfg(any(
target_os = "freebsd",
@@ -95,7 +110,7 @@ pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
))]
{
return bsd::elf_path(profile, name, target_dir);
bsd::elf_path(profile, name, target_dir)
}
#[cfg(not(any(
target_os = "linux",
@@ -107,10 +122,13 @@ pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
)))]
{
return posix::elf_path(profile, name, target_dir);
posix::elf_path(profile, name, target_dir)
}
}
/// Host OS path for an EFI (`.efi`) artifact (`profile`, `name`, optional `target_dir`).
///
/// Backend is chosen at compile time via `cfg!(target_os = …)`, not by the build target triple.
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
#[cfg(target_os = "linux")]
{
@@ -118,11 +136,11 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
}
#[cfg(target_os = "macos")]
{
return macos::efi_path(profile, name, target_dir);
macos::efi_path(profile, name, target_dir)
}
#[cfg(target_os = "windows")]
{
return windows::efi_path(profile, name, target_dir);
windows::efi_path(profile, name, target_dir)
}
#[cfg(any(
target_os = "freebsd",
@@ -131,7 +149,7 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
))]
{
return bsd::efi_path(profile, name, target_dir);
bsd::efi_path(profile, name, target_dir)
}
#[cfg(not(any(
target_os = "linux",
@@ -143,10 +161,13 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
)))]
{
return posix::efi_path(profile, name, target_dir);
posix::efi_path(profile, name, target_dir)
}
}
/// Host OS path for a static library (`profile`, `name`, optional `target_dir`).
///
/// Backend is chosen at compile time via `cfg!(target_os = …)`, not by the build target triple.
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
#[cfg(target_os = "linux")]
{
@@ -154,11 +175,11 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
}
#[cfg(target_os = "macos")]
{
return macos::lib_path(profile, name, target_dir);
macos::lib_path(profile, name, target_dir)
}
#[cfg(target_os = "windows")]
{
return windows::lib_path(profile, name, target_dir);
windows::lib_path(profile, name, target_dir)
}
#[cfg(any(
target_os = "freebsd",
@@ -167,7 +188,7 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
))]
{
return bsd::lib_path(profile, name, target_dir);
bsd::lib_path(profile, name, target_dir)
}
#[cfg(not(any(
target_os = "linux",
@@ -179,10 +200,13 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
)))]
{
return posix::lib_path(profile, name, target_dir);
posix::lib_path(profile, name, target_dir)
}
}
/// Host OS path for a shared library (`profile`, `name`, optional `target_dir`).
///
/// Backend is chosen at compile time via `cfg!(target_os = …)`, not by the build target triple.
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
#[cfg(target_os = "linux")]
{
@@ -190,11 +214,11 @@ pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> S
}
#[cfg(target_os = "macos")]
{
return macos::shared_lib_path(profile, name, target_dir);
macos::shared_lib_path(profile, name, target_dir)
}
#[cfg(target_os = "windows")]
{
return windows::shared_lib_path(profile, name, target_dir);
windows::shared_lib_path(profile, name, target_dir)
}
#[cfg(any(
target_os = "freebsd",
@@ -203,7 +227,7 @@ pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> S
target_os = "dragonfly"
))]
{
return bsd::shared_lib_path(profile, name, target_dir);
bsd::shared_lib_path(profile, name, target_dir)
}
#[cfg(not(any(
target_os = "linux",
@@ -215,6 +239,6 @@ pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> S
target_os = "dragonfly"
)))]
{
return posix::shared_lib_path(profile, name, target_dir);
posix::shared_lib_path(profile, name, target_dir)
}
}

87
src/platform/path_util.rs Normal file
View File

@@ -0,0 +1,87 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::path::{Path, PathBuf};
/// Joins the given directory and file into a single path string, trimming trailing slashes
/// for cross-platform compatibility.
pub fn join_dir(dir: &str, file: &str) -> String {
let trimmed = dir.trim_end_matches(['/', '\\']);
Path::new(trimmed).join(file).to_string_lossy().into_owned()
}
#[cfg_attr(not(windows), allow(dead_code))]
/// Adds `.exe` suffix to the name if it does not already have a recognized executable
/// extension (`.exe`, `.dll`, `.sys`, `.com`, `.efi`) or any extension at all.
pub fn with_exe_suffix(name: &str) -> String {
let lower = name.to_ascii_lowercase();
// Check if name already qualifies for no suffix (has executable ext or any ext)
if lower.ends_with(".exe")
|| lower.ends_with(".dll")
|| lower.ends_with(".sys")
|| lower.ends_with(".com")
|| lower.ends_with(".efi")
|| Path::new(name).extension().is_some()
{
name.to_string()
} else {
format!("{name}.exe")
}
}
/// Default relative binary path: `./target[/<triple>]/<profile>/<name>`.
pub fn default_bin_rel(profile: &str, name: &str, triple_segment: Option<&str>) -> String {
let mut p = PathBuf::from("./target");
if let Some(t) = triple_segment {
p = p.join(t);
}
p.join(profile).join(name).to_string_lossy().into_owned()
}
/// Default relative library path: `./target[/<triple>]/<profile>/<file>`.
pub fn default_lib_rel(profile: &str, file: &str, triple_segment: Option<&str>) -> String {
let mut p = PathBuf::from("./target");
if let Some(t) = triple_segment {
p = p.join(t);
}
p.join(profile).join(file).to_string_lossy().into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn join_dir_trims_both_slashes() {
assert_eq!(
join_dir("out/", "a"),
Path::new("out").join("a").to_string_lossy()
);
assert_eq!(
join_dir(r"out\", "a"),
Path::new("out").join("a").to_string_lossy()
);
}
#[test]
fn with_exe_no_double() {
assert_eq!(with_exe_suffix("hello"), "hello.exe");
assert_eq!(with_exe_suffix("hello.exe"), "hello.exe");
assert_eq!(with_exe_suffix("KERNEL.ELF"), "KERNEL.ELF");
assert_eq!(with_exe_suffix("x.dll"), "x.dll");
}
}

View File

@@ -1,31 +1,48 @@
/// POSIX platform-specific path utilities.
/// Provides functions to build paths for binaries, libraries, and EFI files based on profile and optional target directory.
use super::path_util::{default_bin_rel, default_lib_rel, join_dir};
/// Returns the path to a binary executable for the given profile, name, and optional target directory.
/// If a target directory is provided, it joins the directory with the name; otherwise, it uses the default binary relative path.
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}"),
Some(dir) => join_dir(dir, name),
None => default_bin_rel(profile, name, None),
}
}
/// Returns the path to a static library for the given profile, name, and optional target directory.
/// The library file is named "lib{name}.a". If a target directory is provided, it joins the directory with the file name; otherwise, it uses the default library relative path.
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
let file = format!("lib{name}.a");
match target_dir {
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/lib{name}.a"),
Some(dir) => join_dir(dir, &file),
None => default_lib_rel(profile, &file, None),
}
}
/// Returns the path to an ELF binary for the given profile, name, and optional target directory.
/// Delegates to bin_path since ELF binaries are standard binaries.
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
bin_path(profile, name, target_dir)
}
/// Returns the path to an EFI executable for the given profile, name, and optional target directory.
/// The EFI file is named "{name}.efi". If a target directory is provided, it joins the directory with the file name; otherwise, it uses the default library relative path.
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
let file = format!("{name}.efi");
match target_dir {
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}.efi"),
Some(dir) => join_dir(dir, &file),
None => default_lib_rel(profile, &file, None),
}
}
/// Returns the path to a shared library for the given profile, name, and optional target directory.
/// The shared library file is named "lib{name}.so". If a target directory is provided, it joins the directory with the file name; otherwise, it uses the default library relative path.
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
let file = format!("lib{name}.so");
match target_dir {
Some(dir) => format!("{}/lib{}.so", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/lib{name}.so"),
Some(dir) => join_dir(dir, &file),
None => default_lib_rel(profile, &file, None),
}
}

View File

@@ -15,37 +15,63 @@
// 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 super::path_util::{default_bin_rel, default_lib_rel, join_dir, with_exe_suffix};
/// Returns the path to the binary executable for the given profile, name, and optional target directory.
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
let file = with_exe_suffix(name);
match target_dir {
Some(dir) => format!("{}/{}.exe", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}.exe"),
Some(dir) => join_dir(dir, &file),
None => default_bin_rel(profile, &file, None),
}
}
/// Returns the path to the library file for the given profile, name, and optional target directory.
/// The library name is forced to end with ".lib".
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
// Ensure the library file name ends with ".lib"
let file = if name.to_ascii_lowercase().ends_with(".lib") {
name.to_string()
} else {
format!("{name}.lib")
};
match target_dir {
Some(dir) => format!("{}/{}.lib", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}.lib"),
Some(dir) => join_dir(dir, &file),
None => default_lib_rel(profile, &file, None),
}
}
/// Returns the path to the ELF binary for the given profile, name, and optional target directory.
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}.exe", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}.exe"),
}
bin_path(profile, name, target_dir)
}
/// Returns the path to the EFI binary for the given profile, name, and optional target directory.
/// The binary name is forced to end with ".efi".
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
// Ensure the EFI file name ends with ".efi"
let file = if name.to_ascii_lowercase().ends_with(".efi") {
name.to_string()
} else {
format!("{name}.efi")
};
match target_dir {
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}.efi"),
Some(dir) => join_dir(dir, &file),
None => default_lib_rel(profile, &file, None),
}
}
/// Returns the path to the shared library (DLL) for the given profile, name, and optional target directory.
/// The library name is forced to end with ".dll".
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
// Ensure the shared library file name ends with ".dll"
let file = if name.to_ascii_lowercase().ends_with(".dll") {
name.to_string()
} else {
format!("{name}.dll")
};
match target_dir {
Some(dir) => format!("{}/{}.dll", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}.dll"),
Some(dir) => join_dir(dir, &file),
None => default_lib_rel(profile, &file, None),
}
}

1
src/prelude.rs Normal file
View File

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

View File

@@ -15,9 +15,14 @@
// 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::utils::log::warn;
use crate::prelude::*;
use crate::core::build_config::Config;
use std::path::{Path, PathBuf};
/// Version information parsed from a version string.
///
/// Contains the full version, major, minor, patch, suffix, and suffix_dash components.
pub struct VersionInfo {
pub full: String,
pub major: String,
@@ -27,6 +32,40 @@ pub struct VersionInfo {
pub suffix_dash: String,
}
/// Compares two semver-like strings (`X.Y.Z` with optional `-suffix`, suffix ignored).
///
/// Returns `None` if either side has no parseable numeric major component.
/// Ordering is tool-centric: `compare_semver(tool, required)` → `Less` means tool is older.
pub fn compare_semver(left: &str, right: &str) -> Option<std::cmp::Ordering> {
let l = semver_numeric_parts(left)?;
let r = semver_numeric_parts(right)?;
Some(l.cmp(&r))
}
fn semver_numeric_parts(version: &str) -> Option<(u64, u64, u64)> {
let base = version
.trim()
.split_once('-')
.map(|(h, _)| h)
.unwrap_or(version.trim());
if base.is_empty() {
return None;
}
let mut parts = base.split('.');
let major = parts.next()?.parse::<u64>().ok()?;
let minor = parts.next().unwrap_or("0").parse::<u64>().unwrap_or(0);
let patch = parts
.next()
.unwrap_or("0")
.chars()
.take_while(|c| c.is_ascii_digit())
.collect::<String>()
.parse::<u64>()
.unwrap_or(0);
Some((major, minor, patch))
}
/// Parses a version string into VersionInfo struct.
pub fn parse_version_info(version: &str) -> VersionInfo {
let mut full = version.trim().to_string();
if full.is_empty() {
@@ -55,6 +94,7 @@ pub fn parse_version_info(version: &str) -> VersionInfo {
}
}
/// Substitutes profile, name, and version variables into a template string.
pub fn substitute_vars(template: &str, info: &VersionInfo, profile: &str, name: &str) -> String {
let s = template
.replace("{profile}", profile)
@@ -62,6 +102,7 @@ pub fn substitute_vars(template: &str, info: &VersionInfo, profile: &str, name:
substitute_version_vars(&s, info)
}
/// Substitutes version-specific variables into a template string.
pub fn substitute_version_vars(template: &str, info: &VersionInfo) -> String {
template
.replace("{version}", &info.full)
@@ -72,6 +113,7 @@ pub fn substitute_version_vars(template: &str, info: &VersionInfo) -> String {
.replace("{version_suffix_dash}", &info.suffix_dash)
}
/// Normalizes a target string to a relative path for artifact output directory.
pub fn normalize_target(target: &str, profile: &str) -> Option<String> {
let trimmed = normalize_target_os(target.trim());
if trimmed.is_empty() {
@@ -81,19 +123,111 @@ pub fn normalize_target(target: &str, profile: &str) -> Option<String> {
}
}
/// Returns the native host target directory relative to project root for given profile.
pub fn native_host_target_rel(profile: &str) -> PathBuf {
if cfg!(any(
target_os = "linux",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)) {
Path::new("target")
.join(default_target_triple())
.join(profile)
} else {
Path::new("target").join(profile)
}
}
/// Resolves the artifact output directory for a project/workspace build.
///
/// # Parameters
/// - `project_root`: Package root.
/// - `workspace_root`: If set, artifacts go under the workspace `target/`.
/// - `profile`: Build profile directory segment.
/// - `build_target`: Target triple or short name (may be empty).
/// - `out_dir`: Explicit `out_dir` from config (wins when non-empty).
/// - `has_explicit_target`: Whether CLI/config set a target (changes layout).
///
/// # Returns
/// Absolute directory path as a string.
pub fn resolve_artifact_target_dir(
project_root: &Path,
workspace_root: Option<&Path>,
profile: &str,
build_target: &str,
out_dir: &str,
has_explicit_target: bool,
) -> String {
if !out_dir.trim().is_empty() {
let p = Path::new(out_dir.trim());
return if p.is_absolute() {
out_dir.trim().to_string()
} else {
project_root.join(p).to_string_lossy().into_owned()
};
}
if let Some(ws) = workspace_root {
let triple = if build_target.trim().is_empty() {
default_target_triple()
} else {
let n = normalize_target_os(build_target.trim());
if n.is_empty() {
default_target_triple()
} else {
n.to_string()
}
};
return ws
.join("target")
.join(triple)
.join(profile)
.to_string_lossy()
.into_owned();
}
let base = if has_explicit_target {
match normalize_target(build_target, profile) {
Some(rel) => PathBuf::from(rel),
None => native_host_target_rel(profile),
}
} else {
native_host_target_rel(profile)
};
project_root.join(base).to_string_lossy().into_owned()
}
/// Normalizes kind string to standard value, defaulting to "bin" if empty.
pub fn normalize_kind(kind: &str) -> &str {
let trimmed = kind.trim();
if trimmed.is_empty() { "bin" } else { trimmed }
}
/// Checks if the kind is a flat binary artifact.
pub fn is_flat_bin(kind: &str) -> bool {
kind == "flat-bin"
}
/// Returns true for kinds that skip the normal link/archive step: `none`, `custom`, or `flat-bin`.
///
/// Callers that still need a flat-binary path use [`is_flat_bin`] separately
/// (e.g. `is_compile_only(kind) && !is_flat_bin(kind)` before an early return).
///
/// # Parameters
/// - `kind`: Normalized artifact kind string.
///
/// # Returns
/// `true` for `none`, `custom`, or `flat-bin`.
///
/// Callers that still need a flat-binary link use [`is_flat_bin`] separately
/// (e.g. `is_compile_only(kind) && !is_flat_bin(kind)` before early return).
pub fn is_compile_only(kind: &str) -> bool {
matches!(kind, "none" | "custom" | "flat-bin")
}
/// Normalizes platform string, returning None if empty.
pub fn normalize_platform(platform: &str) -> Option<&str> {
let trimmed = platform.trim();
if trimmed.is_empty() {
@@ -103,22 +237,30 @@ pub fn normalize_platform(platform: &str) -> Option<&str> {
}
}
/// Expands short OS names (`linux`/`macos`/`windows`) to default triples; leaves others as-is.
///
/// # Parameters
/// - `target`: Short name, full triple, or empty.
///
/// # Returns
/// Canonical triple string, empty input unchanged, unknown bare names returned with a warning.
pub fn normalize_target_os(target: &str) -> &str {
match target {
"" => "",
"linux" => "x86_64-unknown-linux-gnu",
"macos" => "x86_64-apple-darwin",
"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
}
}
}
/// Returns the default target triple for the current host platform.
pub fn default_target_triple() -> String {
let arch = std::env::consts::ARCH;
if cfg!(target_os = "linux") {
@@ -149,6 +291,7 @@ pub fn default_target_triple() -> String {
}
}
/// Prepends a --target flag to flags vector if using Clang and not already present.
pub fn prepend_clang_target_flag(flags: &mut Vec<String>, target: Option<&str>, tool: &str) {
let Some(target) = target.map(str::trim).filter(|t| !t.is_empty()) else {
return;
@@ -166,6 +309,7 @@ pub fn prepend_clang_target_flag(flags: &mut Vec<String>, target: Option<&str>,
}
}
/// Retrieves a string value from config, falling back to empty string if missing.
pub fn get_config_str(config: &Config, key: &str) -> String {
config
.get(key)
@@ -174,6 +318,7 @@ pub fn get_config_str(config: &Config, key: &str) -> String {
.to_string()
}
/// Retrieves profile-specific table from config.
pub fn profile_table<'a>(config: &'a Config, profile: &str) -> Option<&'a toml::value::Table> {
config
.get("build")
@@ -182,6 +327,7 @@ pub fn profile_table<'a>(config: &'a Config, profile: &str) -> Option<&'a toml::
.and_then(|v| v.as_table())
}
/// Retrieves an optional string value from config.
pub fn get_config_opt(config: &Config, key: &str) -> Option<String> {
let value = config.get(key)?.as_str()?;
let trimmed = value.trim();
@@ -192,6 +338,7 @@ pub fn get_config_opt(config: &Config, key: &str) -> Option<String> {
}
}
/// Retrieves string value with profile override support.
pub fn get_string_with_profile(config: &Config, field: &str, profile: &str) -> String {
let base = get_config_str(config, &format!("build.{field}"));
let Some(table) = profile_table(config, profile) else {
@@ -206,6 +353,7 @@ pub fn get_string_with_profile(config: &Config, field: &str, profile: &str) -> S
}
}
/// Retrieves list value from config.
pub fn get_config_list(config: &Config, key: &str) -> Vec<String> {
config
.get(key)
@@ -219,6 +367,7 @@ pub fn get_config_list(config: &Config, key: &str) -> Vec<String> {
.unwrap_or_default()
}
/// Retrieves list value with profile override support.
pub fn get_list_with_profile(config: &Config, field: &str, profile: &str) -> Vec<String> {
let mut out = get_config_list(config, &format!("build.{field}"));
if let Some(table) = profile_table(config, profile)
@@ -234,6 +383,7 @@ pub fn get_list_with_profile(config: &Config, field: &str, profile: &str) -> Vec
out
}
/// Helper function to check if a string contains a pattern ignoring case.
fn contains_ignore_case(text: &str, pattern: &str) -> bool {
if pattern.len() > text.len() {
return false;
@@ -243,6 +393,7 @@ fn contains_ignore_case(text: &str, pattern: &str) -> bool {
.any(|w| w.eq_ignore_ascii_case(pattern.as_bytes()))
}
/// Checks if target is a bare-metal or embedded target.
pub fn is_bare_metal_target(target: Option<&str>) -> bool {
match target {
Some(t) => {
@@ -256,6 +407,7 @@ pub fn is_bare_metal_target(target: Option<&str>) -> bool {
}
}
/// Returns default flags for a given profile.
pub fn default_profile_flags(profile: &str) -> &'static [&'static str] {
match profile {
"release" => &["-O3", "-DNDEBUG"],
@@ -271,6 +423,7 @@ pub fn default_profile_flags(profile: &str) -> &'static [&'static str] {
}
}
/// Retrieves boolean value with profile override support.
pub fn get_bool_with_profile(config: &Config, field: &str, profile: &str, default: bool) -> bool {
let base = config
.get(&format!("build.{field}"))
@@ -280,6 +433,7 @@ pub fn get_bool_with_profile(config: &Config, field: &str, profile: &str, defaul
profile_val.or(base).unwrap_or(default)
}
/// Retrieves language from config with profile support.
pub fn get_language_with_profile(config: &Config, profile: &str) -> Result<String, String> {
if let Some(table) = profile_table(config, profile)
&& let Some(value) = table.get("language")
@@ -292,10 +446,12 @@ pub fn get_language_with_profile(config: &Config, profile: &str) -> Result<Strin
}
}
/// Retrieves language from config with profile support, defaulting to "c" on error.
pub fn get_language_with_profile_or_default(config: &Config, profile: &str) -> String {
get_language_with_profile(config, profile).unwrap_or_else(|_| "c".to_string())
}
/// Parses language value from config, supporting string or array format.
pub fn parse_language_value(value: &toml::Value, key: &str) -> Result<String, String> {
if let Some(s) = value.as_str() {
let trimmed = s.trim();
@@ -324,6 +480,7 @@ pub fn parse_language_value(value: &toml::Value, key: &str) -> Result<String, St
Ok(parts.join(","))
}
/// Resolves compiler name based on language, environment, and toolchain overrides.
pub fn resolve_compiler(
language: &str,
compiler: &str,
@@ -342,6 +499,7 @@ pub fn resolve_compiler(
}
}
/// Checks environment variables for compiler override.
fn env_override_compiler(lang: &str) -> Option<String> {
if let Ok(value) = std::env::var("DCR_COMPILER") {
let trimmed = value.trim();
@@ -375,6 +533,7 @@ fn env_override_compiler(lang: &str) -> Option<String> {
None
}
/// Checks toolchain overrides for compiler.
fn toolchain_override_compiler(
lang: &str,
tc_cc: Option<&str>,
@@ -393,6 +552,7 @@ fn toolchain_override_compiler(
tc_cc.map(|v| v.to_string())
}
/// Maps assembler compiler name to canonical form.
fn map_asm_compiler(compiler: &str) -> String {
match compiler.to_lowercase().as_str() {
"gas" | "gnu-as" => "as".to_string(),
@@ -403,6 +563,7 @@ fn map_asm_compiler(compiler: &str) -> String {
}
}
/// Determines primary language from comma-separated list.
pub fn primary_language(language: &str) -> String {
let tokens: Vec<&str> = language
.split(',')
@@ -421,6 +582,7 @@ pub fn primary_language(language: &str) -> String {
language.to_lowercase()
}
/// Resolves tool from environment variable or fallback.
pub fn resolve_tool(env_key: &str, fallback: Option<&str>) -> Option<String> {
if let Ok(value) = std::env::var(env_key) {
let trimmed = value.trim();
@@ -431,6 +593,7 @@ pub fn resolve_tool(env_key: &str, fallback: Option<&str>) -> Option<String> {
fallback.map(|v| v.to_string())
}
/// Resolves pkg-config flags for packages.
pub fn resolve_pkg_config_flags(
pkgs: &[String],
base_cflags: &[String],
@@ -447,6 +610,7 @@ pub fn resolve_pkg_config_flags(
Ok((cflags, ldflags))
}
/// Resolves pkg-config flags, falling back on error.
pub fn resolve_pkg_config_flags_lossy(
pkgs: &[String],
base_cflags: &[String],
@@ -461,6 +625,7 @@ pub fn resolve_pkg_config_flags_lossy(
}
}
/// Runs pkg-config command and returns output.
pub fn run_pkg_config(pkg: &str, arg: &str) -> Result<String, String> {
let output = std::process::Command::new("pkg-config")
.arg(arg)
@@ -474,6 +639,7 @@ pub fn run_pkg_config(pkg: &str, arg: &str) -> Result<String, String> {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
// The split_flags function parses pkg-config output, handling quoted strings and whitespace to split into individual flags.
fn split_flags(value: &str) -> Vec<String> {
let mut out = Vec::new();
let mut current = String::new();
@@ -513,8 +679,10 @@ fn split_flags(value: &str) -> Vec<String> {
mod tests {
use super::*;
/// Test for normalize_target_short_names function.
#[test]
fn normalize_target_short_names() {
assert_eq!(normalize_target_os(""), "");
assert_eq!(normalize_target_os("linux"), "x86_64-unknown-linux-gnu");
assert_eq!(normalize_target_os("macos"), "x86_64-apple-darwin");
assert_eq!(normalize_target_os("windows"), "x86_64-pc-windows-msvc");
@@ -525,20 +693,31 @@ mod tests {
assert_eq!(normalize_target_os("unknown"), "unknown");
}
/// Test for default_target_triple_uses_host_arch function.
#[test]
fn default_target_triple_uses_host_arch() {
let target = default_target_triple();
if cfg!(target_os = "linux") {
let env = if cfg!(target_env = "musl") {
"musl"
} else {
"gnu"
};
assert_eq!(
target,
format!("{}-unknown-linux-gnu", std::env::consts::ARCH)
format!("{}-unknown-linux-{env}", std::env::consts::ARCH)
);
} else if cfg!(target_os = "macos") {
assert_eq!(target, format!("{}-apple-darwin", std::env::consts::ARCH));
} else if cfg!(target_os = "windows") {
let env = if cfg!(target_env = "gnu") {
"gnu"
} else {
"msvc"
};
assert_eq!(
target,
format!("{}-pc-windows-msvc", std::env::consts::ARCH)
format!("{}-pc-windows-{env}", std::env::consts::ARCH)
);
} else if cfg!(any(
target_os = "freebsd",
@@ -559,6 +738,28 @@ mod tests {
}
}
/// Test for resolve_artifact_target_dir_native_and_workspace function.
#[test]
fn resolve_artifact_target_dir_native_and_workspace() {
let root = Path::new("/proj");
let ws = Path::new("/ws");
let host = default_target_triple();
let native = resolve_artifact_target_dir(root, None, "debug", &host, "", false);
let expected_native = root.join(native_host_target_rel("debug"));
assert_eq!(native, expected_native.to_string_lossy());
let explicit =
resolve_artifact_target_dir(root, None, "debug", "x86_64-unknown-linux-gnu", "", true);
assert!(explicit.contains("x86_64-unknown-linux-gnu"));
let ws_dir = resolve_artifact_target_dir(root, Some(ws), "debug", &host, "", false);
assert!(ws_dir.starts_with("/ws") || ws_dir.contains("ws"));
assert!(ws_dir.contains(&host) || cfg!(windows));
let out = resolve_artifact_target_dir(root, None, "debug", "", "dist", false);
assert!(out.contains("dist"));
}
/// Test for prepend_clang_target_flag_adds_once_for_clang function.
#[test]
fn prepend_clang_target_flag_adds_once_for_clang() {
let mut flags = vec!["-O2".to_string()];
@@ -573,6 +774,7 @@ mod tests {
);
}
/// Test for prepend_clang_target_flag_ignores_non_clang_tools function.
#[test]
fn prepend_clang_target_flag_ignores_non_clang_tools() {
let mut flags = vec!["-O2".to_string()];
@@ -580,6 +782,14 @@ mod tests {
assert_eq!(flags, vec!["-O2".to_string()]);
}
#[test]
fn native_clang_target_is_not_added_without_explicit_target() {
let mut flags = vec!["-O3".to_string()];
prepend_clang_target_flag(&mut flags, None, "clang");
assert_eq!(flags, vec!["-O3".to_string()]);
}
/// Test for parse_version_parts function.
#[test]
fn parse_version_parts() {
let info = parse_version_info("1.2.3-beta");
@@ -591,19 +801,32 @@ mod tests {
assert_eq!(info.suffix_dash, "-beta");
}
#[test]
fn compare_semver_orders_and_ignores_prerelease() {
use std::cmp::Ordering;
assert_eq!(compare_semver("0.8.4", "0.8.4"), Some(Ordering::Equal));
assert_eq!(compare_semver("0.8.3", "0.8.4"), Some(Ordering::Less));
assert_eq!(compare_semver("0.9.0", "0.8.4"), Some(Ordering::Greater));
assert_eq!(compare_semver("1.0.0-dev", "1.0.0"), Some(Ordering::Equal));
assert_eq!(compare_semver("not-a-version", "0.1.0"), None);
}
/// Test for normalize_target_with_profile function.
#[test]
fn normalize_target_with_profile() {
let linux = normalize_target_os("linux");
assert_eq!(
normalize_target("linux", "debug"),
Some("target/x86_64-unknown-linux-gnu/debug".into())
Some(format!("target/{linux}/debug"))
);
assert_eq!(
normalize_target("x86_64-unknown-linux-gnu", "release"),
Some("target/x86_64-unknown-linux-gnu/release".into())
normalize_target(linux, "release"),
Some(format!("target/{linux}/release"))
);
assert_eq!(normalize_target("", "debug"), None);
}
/// Test for normalize_kind_empty_defaults_to_bin function.
#[test]
fn normalize_kind_empty_defaults_to_bin() {
assert_eq!(normalize_kind(""), "bin");
@@ -612,6 +835,7 @@ mod tests {
assert_eq!(normalize_kind(" elf "), "elf");
}
/// Test for normalize_platform_empty_is_none function.
#[test]
fn normalize_platform_empty_is_none() {
assert_eq!(normalize_platform(""), None);
@@ -620,6 +844,7 @@ mod tests {
assert_eq!(normalize_platform(" efi "), Some("efi"));
}
/// Test for default_profile_flags_by_profile function.
#[test]
fn default_profile_flags_by_profile() {
assert!(default_profile_flags("debug").contains(&"-O0"));
@@ -629,6 +854,7 @@ mod tests {
assert!(default_profile_flags("unknown").is_empty());
}
/// Test for is_bare_metal_detects_embedded_targets function.
#[test]
fn is_bare_metal_detects_embedded_targets() {
assert!(is_bare_metal_target(Some("aarch64-none-elf")));

View File

@@ -15,35 +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;
// Other
#[allow(dead_code)]
pub fn colored(msg: &str, style: &str) -> String {
format!("{style}{msg}{RESET}")
}
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

@@ -16,8 +16,13 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::io;
use std::io::Write;
use std::path::{Path, PathBuf};
/// Resolves the path to the user's home directory across different operating systems.
///
/// Checks the `HOME` environment variable (Unix-like systems) first,
/// then falls back to `USERPROFILE` (Windows).
pub fn home_dir() -> Option<PathBuf> {
if let Ok(home) = std::env::var("HOME") {
return Some(PathBuf::from(home));
@@ -28,6 +33,7 @@ pub fn home_dir() -> Option<PathBuf> {
None
}
/// Converts a byte slice into a lowercase hexadecimal string.
pub fn to_hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
@@ -36,6 +42,7 @@ pub fn to_hex(bytes: &[u8]) -> String {
out
}
/// Lists all entry names (files and directories) inside a target directory.
#[allow(dead_code)]
pub fn check_dir(dir: Option<&str>) -> io::Result<Vec<String>> {
let path: PathBuf = match dir {
@@ -52,6 +59,7 @@ pub fn check_dir(dir: Option<&str>) -> io::Result<Vec<String>> {
Ok(items)
}
/// Searches upwards from a starting path to locate the root directory containing `dcr.toml`.
pub fn find_project_root(start: &Path) -> io::Result<Option<PathBuf>> {
let mut current = start.to_path_buf();
loop {
@@ -65,6 +73,15 @@ pub fn find_project_root(start: &Path) -> io::Result<Option<PathBuf>> {
Ok(None)
}
/// Executes a closure inside a specified directory and restores the original cwd afterwards.
///
/// # Parameters
/// - `dir`: Directory to `cd` into for the duration of `f`.
/// - `f`: Work to run with that cwd (its `Result` is returned as-is).
///
/// # Returns
/// The closure's result, or an error if getting/changing directory fails.
/// Cwd is restored even when `f` returns `Err`.
pub fn with_dir<F, T>(dir: &Path, f: F) -> Result<T, String>
where
F: FnOnce() -> Result<T, String>,
@@ -72,6 +89,178 @@ where
let prev = std::env::current_dir().map_err(|_| "Failed to get current dir".to_string())?;
std::env::set_current_dir(dir).map_err(|_| "Failed to change directory".to_string())?;
let result = f();
// Restore previous working directory regardless of closure result.
let _ = std::env::set_current_dir(prev);
result
}
/// Strips Windows verbatim/extended-length prefixes (e.g. `\\?\`, `\\.\`) and standardizes slashes.
///
/// Normalize path separators for cross-platform safety and removes platform-specific extended prefixes.
pub fn strip_verbatim_prefix(path: PathBuf) -> PathBuf {
let s = path.to_string_lossy();
let mut cleaned = s.replace('\\', "/");
if let Some(rest) = cleaned.strip_prefix("//?/") {
cleaned = rest.to_string();
} else if let Some(rest) = cleaned.strip_prefix("//./") {
cleaned = rest.to_string();
} else if let Some(rest) = cleaned.strip_prefix(r"\\?\") {
cleaned = rest.replace('\\', "/");
} else if let Some(rest) = cleaned.strip_prefix(r"\\.\") {
cleaned = rest.replace('\\', "/");
}
if cleaned.len() >= 3 && cleaned.as_bytes()[0] == b'/' && cleaned.as_bytes()[2] == b':' {
cleaned = cleaned[1..].to_string();
}
if cfg!(windows) {
PathBuf::from(cleaned.replace('/', "\\"))
} else {
PathBuf::from(cleaned)
}
}
/// Canonicalizes a path to its absolute form and cleans up Windows extended-length prefixes.
///
/// Falls back to joining relative paths with the current working directory if canonicalization fails.
pub fn canonicalize_path(path: &Path) -> PathBuf {
let raw = std::fs::canonicalize(path).unwrap_or_else(|_| {
if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()
.map(|cwd| cwd.join(path))
.unwrap_or_else(|_| path.to_path_buf())
}
});
strip_verbatim_prefix(raw)
}
/// Resolves a path relative to a root directory and returns a canonicalized absolute path.
pub fn absolute_join(root: &Path, path: &Path) -> PathBuf {
let joined = if path.is_absolute() {
path.to_path_buf()
} else {
root.join(path)
};
canonicalize_path(&joined)
}
/// Ensures the `.dcr` internal metadata directory and its `ide` subfolder exist within a project root.
pub fn ensure_dcr_dir(root: &Path) -> io::Result<PathBuf> {
let dcr = root.join(".dcr");
std::fs::create_dir_all(dcr.join("ide"))?;
Ok(dcr)
}
/// Writes `contents` via a `.tmp.<pid>` file then renames into place (best-effort replace).
///
/// # Parameters
/// - `path`: Final destination path (parent dirs are created).
/// - `contents`: Bytes to write.
///
/// # Returns
/// `Ok(())` after a successful rename (with fallbacks if the target already exists).
pub fn atomic_write(path: &Path, contents: impl AsRef<[u8]>) -> io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let pid = std::process::id();
let file_name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "tmp".to_string());
let tmp = path.with_file_name(format!("{file_name}.tmp.{pid}"));
{
let mut f = std::fs::File::create(&tmp)?;
f.write_all(contents.as_ref())?;
let _ = f.sync_all();
}
match std::fs::rename(&tmp, path) {
Ok(()) => Ok(()),
Err(e) => {
// Handle cross-device moves or existing file replacement fallbacks.
if path.exists() {
let _ = std::fs::remove_file(path);
match std::fs::rename(&tmp, path) {
Ok(()) => Ok(()),
Err(e2) => {
let _ = std::fs::remove_file(&tmp);
Err(e2)
}
}
} else {
let _ = std::fs::remove_file(&tmp);
Err(e)
}
}
}
}
/// Ensures that `.gitignore` exists in the project directory and includes the `.dcr/` output rule.
///
/// If `.gitignore` does not exist, creates one with standard `/target` and `.dcr/` entries.
pub fn ensure_gitignore_has_dcr(project_dir: &Path) -> io::Result<()> {
let path = project_dir.join(".gitignore");
if path.exists() {
let content = std::fs::read_to_string(&path)?;
let has = content.lines().any(|l| {
let t = l.trim();
t == ".dcr/" || t == ".dcr" || t == "/.dcr/" || t == "/.dcr"
});
if !has {
let mut new_content = content;
if !new_content.ends_with('\n') && !new_content.is_empty() {
new_content.push('\n');
}
new_content.push_str(".dcr/\n");
std::fs::write(&path, new_content)?;
}
} else {
std::fs::write(&path, "/target\n.dcr/\n")?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn canonicalize_path_absolute() {
let cwd = std::env::current_dir().unwrap();
let got = canonicalize_path(&cwd);
assert!(got.is_absolute());
}
#[test]
fn canonicalize_path_relative_fallback() {
let p = Path::new("does-not-exist-xyz-12345");
let got = canonicalize_path(p);
assert!(got.is_absolute());
}
#[test]
fn atomic_write_roundtrip() {
let dir = std::env::temp_dir().join(format!("dcr_atomic_{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let path = dir.join("out.json");
atomic_write(&path, b"hello").unwrap();
assert_eq!(fs::read_to_string(&path).unwrap(), "hello");
atomic_write(&path, b"world").unwrap();
assert_eq!(fs::read_to_string(&path).unwrap(), "world");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn ensure_dcr_dir_creates_ide() {
let dir = std::env::temp_dir().join(format!("dcr_dir_{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let dcr = ensure_dcr_dir(&dir).unwrap();
assert!(dcr.is_dir());
assert!(dcr.join("ide").is_dir());
let _ = fs::remove_dir_all(&dir);
}
}

View File

@@ -1,6 +1,8 @@
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.
///
/// This function executes `git --version` and returns `true` if the command succeeds, `false` otherwise.
pub fn is_git_available() -> bool {
Command::new("git")
.arg("--version")
@@ -9,6 +11,7 @@ pub fn is_git_available() -> bool {
.unwrap_or(false)
}
/// Initializes a new Git repository at the specified path.
pub fn git_init(path: &Path) -> Result<(), String> {
if !is_git_available() {
return Err("git executable not found in PATH".to_string());

124
src/utils/help.rs Normal file
View File

@@ -0,0 +1,124 @@
use super::cli_styles::{BOLD_CYAN, BOLD_GREEN, Colorize};
use super::output;
pub struct HelpBuilder {
title: String,
description: Option<String>,
sections: Vec<Section>,
tip: Option<String>,
}
pub struct HelpSection<'a> {
help: &'a mut HelpBuilder,
index: usize,
}
struct Section {
title: String,
entries: Vec<(String, String)>,
}
impl HelpBuilder {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
description: None,
sections: Vec::new(),
tip: None,
}
}
pub fn description(&mut self, text: impl Into<String>) -> &mut Self {
self.description = Some(text.into());
self
}
/// Add an example to a dedicated `EXAMPLES` section.
pub fn example(&mut self, text: impl Into<String>) -> &mut Self {
let text = text.into();
if let Some(section) = self.sections.iter_mut().find(|s| s.title == "EXAMPLES") {
section.entries.push((text, String::new()));
} else {
self.sections.push(Section {
title: "EXAMPLES".to_owned(),
entries: vec![(text, String::new())],
});
}
self
}
pub fn note(&mut self, text: impl Into<String>) -> &mut Self {
let text = text.into();
match &mut self.description {
Some(description) if !description.is_empty() => {
description.push('\n');
description.push_str(&text);
}
Some(description) => description.push_str(&text),
None => self.description = Some(text),
}
self
}
pub fn section(&mut self, title: impl Into<String>) -> HelpSection<'_> {
self.sections.push(Section {
title: title.into(),
entries: Vec::new(),
});
HelpSection {
index: self.sections.len() - 1,
help: self,
}
}
pub fn tip(&mut self, text: impl Into<String>) -> &mut Self {
self.tip = Some(text.into());
self
}
pub fn render(&self) {
if output::mode() == output::Mode::Json {
let sections: Vec<_> = self.sections.iter().map(|s| serde_json::json!({"title": s.title, "items": s.entries.iter().map(|(n,d)| serde_json::json!({"name":n,"description":d})).collect::<Vec<_>>() })).collect();
let event = serde_json::json!({
"schema_version": 1,
"event": "help",
"title": self.title,
"description": self.description,
"sections": sections,
"tip": self.tip
});
output::write_stdout_raw(&format!("{event}\n"));
return;
}
let mut rendered = String::new();
rendered.push_str(&format!("{}\n", self.title.style(BOLD_GREEN)));
if let Some(d) = &self.description {
rendered.push_str(&format!("{}\n\n", d));
}
for s in &self.sections {
rendered.push_str(&format!("{}:\n", s.title.style(BOLD_GREEN)));
let width = s.entries.iter().map(|(n, _)| n.len()).max().unwrap_or(0);
for (n, d) in &s.entries {
if d.is_empty() {
rendered.push_str(&format!(" {}\n", n.style(BOLD_CYAN)));
} else {
rendered.push_str(&format!(
" {} {d}\n",
format!("{n:<width$}").style(BOLD_CYAN)
));
}
}
rendered.push('\n');
}
if let Some(t) = &self.tip {
rendered.push_str(&format!("{}:\n {t}\n", "TIP".style(BOLD_GREEN)));
}
output::write_stdout(&rendered);
}
}
impl HelpSection<'_> {
pub fn item(self, name: impl Into<String>, description: impl Into<String>) -> Self {
self.help.sections[self.index]
.entries
.push((name.into(), description.into()));
self
}
pub fn example(self, command: impl Into<String>) -> Self {
self.item(command, "")
}
}

Some files were not shown because too many files have changed in this diff Show More