61 Commits

Author SHA1 Message Date
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
588e4dd13f feat(build): archive feature, progress UI, and workspace path fixes 2026-07-23 14:00:00 +08:00
689fea7465 Merge pull request #28 from dexoron/dev
v0.8.1
2026-07-21 18:12:56 +08:00
a9dd8c1631 feat(build): add flat-bin kind and FAT [archive] packing for OS-dev 2026-07-21 17:45:02 +08:00
9d030cba0c fix(build): skip linking/archiving for custom and none project kinds 2026-07-18 20:21:41 +08:00
1dcc8e614a Merge pull request #25 from dexoron/dev
v0.8.0
2026-07-18 20:05:12 +08:00
9eaa4635d5 refactor: modularize build core engine, update workspaces, cache, and test suite 2026-07-18 20:02: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
9326e23a53 feat: add native Qt support and dcr lint command; refactor architecture and integration tests 2026-06-17 12:39:22 +08:00
21932ac94a feat(build): add freestanding, lto, strip, panic-abort, and codegen-units support in dcr.toml 2026-06-16 16:42:56 +08:00
97db82e4f8 Merge pull request #20 from dexoron/dev
v0.7.3
2026-06-15 22:36:12 +08:00
3d596d7280 Update install docs 2026-06-15 22:35:38 +08:00
5cb4473bd1 Update README.md 2026-06-15 22:35:06 +08:00
130 changed files with 13594 additions and 5212 deletions

View File

@@ -5,10 +5,30 @@ on:
branches: branches:
- master - master
- main - main
- dev
paths:
- 'src/**'
- 'tests/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.github/workflows/ci.yml'
pull_request: 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: jobs:
checks: checks:
name: fmt / clippy / check
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -18,11 +38,129 @@ jobs:
with: with:
components: rustfmt, clippy 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 - name: Format
run: cargo fmt --all --check run: cargo fmt --all --check
- name: Lint - name: Clippy (default features)
run: cargo clippy --all-targets -- -D warnings
- name: Clippy (all features)
run: cargo clippy --all-targets --all-features -- -D warnings run: cargo clippy --all-targets --all-features -- -D warnings
- name: Check - name: Check (all features)
run: cargo check --all-targets --all-features run: cargo check --all-targets --all-features
test:
name: tests (${{ matrix.os }})
needs: checks
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- 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'
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
clang \
nasm \
clang-format \
clang-tidy \
git \
llvm
- name: Install macOS tools
if: runner.os == 'macOS'
run: |
brew install nasm llvm || true
echo "$(brew --prefix llvm)/bin" >> "$GITHUB_PATH"
- name: Install Windows tools
if: runner.os == 'Windows'
shell: pwsh
run: |
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
- name: Integration tests (default features)
run: cargo test --tests -- --nocapture
- name: Integration tests (archive feature)
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
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- 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 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" description: "Update Homebrew tap"
type: boolean type: boolean
default: false default: false
run_s3:
description: "Mirror Release to Yandex Object Storage (S3)"
type: boolean
default: false
permissions: permissions:
contents: write contents: write
@@ -93,20 +97,22 @@ jobs:
set -euo pipefail set -euo pipefail
tag="${{ needs.setup.outputs.version }}" tag="${{ needs.setup.outputs.version }}"
notes_file=".github/release-notes.md" notes_file=".github/release-notes.md"
if grep -q "^## \\[${tag}\\]" CHANGELOG.md; then # Strip -dev suffix to match base version in CHANGELOG
changelog_tag="${tag%-dev}"
if grep -q "^## \\[${changelog_tag}\\]" CHANGELOG.md; then
# Extract release name from header: ## [0.7.1] - 2026-06-02 "Title" # Extract release name from header: ## [0.7.1] - 2026-06-02 "Title"
release_name=$(awk -v ver="$tag" ' release_name=$(awk -v search_ver="$changelog_tag" -v display_ver="$tag" '
$0 ~ "^## \\[" ver "\\]" { $0 ~ "^## \\[" search_ver "\\]" {
match($0, /"([^"]+)"/, arr) match($0, /"([^"]+)"/, arr)
if (arr[1] != "") { if (arr[1] != "") {
print "v" ver " - " arr[1] print "v" display_ver " - " arr[1]
} else { } else {
print "v" ver print "v" display_ver
} }
} }
' CHANGELOG.md) ' CHANGELOG.md)
echo "release_name=${release_name}" >> "$GITHUB_OUTPUT" echo "release_name=${release_name}" >> "$GITHUB_OUTPUT"
awk -v ver="$tag" ' awk -v ver="$changelog_tag" '
$0 ~ "^## \\[" ver "\\]" {found=1; next} $0 ~ "^## \\[" ver "\\]" {found=1; next}
found && $0 ~ "^## \\[" {exit} found && $0 ~ "^## \\[" {exit}
found {print} found {print}
@@ -221,9 +227,9 @@ jobs:
- name: Build - name: Build
run: | run: |
if [ "${{ matrix.target }}" != "x86_64-unknown-linux-gnu" ]; then if [ "${{ matrix.target }}" != "x86_64-unknown-linux-gnu" ]; then
cargo zigbuild --release --target ${{ matrix.target }} cargo zigbuild --release --features archive --target ${{ matrix.target }}
else else
cargo build --release --target ${{ matrix.target }} cargo build --release --features archive --target ${{ matrix.target }}
fi fi
- name: Prepare Artifact - name: Prepare Artifact
@@ -296,9 +302,9 @@ jobs:
run: | run: |
cargo install cargo-deb cargo install cargo-deb
if [ "${{ matrix.target }}" != "x86_64-unknown-linux-gnu" ]; then if [ "${{ matrix.target }}" != "x86_64-unknown-linux-gnu" ]; then
cargo zigbuild --release --target ${{ matrix.target }} cargo zigbuild --release --features archive --target ${{ matrix.target }}
else else
cargo build --release --target ${{ matrix.target }} cargo build --release --features archive --target ${{ matrix.target }}
fi fi
cargo deb --no-build --target ${{ matrix.target }} cargo deb --no-build --target ${{ matrix.target }}
mkdir -p dist-packages && cp target/${{ matrix.target }}/debian/*.deb dist-packages/ mkdir -p dist-packages && cp target/${{ matrix.target }}/debian/*.deb dist-packages/
@@ -309,9 +315,9 @@ jobs:
run: | run: |
cargo install cargo-generate-rpm cargo install cargo-generate-rpm
if [ "${{ matrix.target }}" != "x86_64-unknown-linux-gnu" ]; then if [ "${{ matrix.target }}" != "x86_64-unknown-linux-gnu" ]; then
cargo zigbuild --release --target ${{ matrix.target }} cargo zigbuild --release --features archive --target ${{ matrix.target }}
else else
cargo build --release --target ${{ matrix.target }} cargo build --release --features archive --target ${{ matrix.target }}
fi fi
cargo generate-rpm --target ${{ matrix.target }} cargo generate-rpm --target ${{ matrix.target }}
mkdir -p dist-packages && cp target/${{ matrix.target }}/generate-rpm/*.rpm dist-packages/ mkdir -p dist-packages && cp target/${{ matrix.target }}/generate-rpm/*.rpm dist-packages/
@@ -320,7 +326,7 @@ jobs:
- name: Build Snap Package - name: Build Snap Package
if: matrix.package_type == 'snap' if: matrix.package_type == 'snap'
run: | run: |
cargo build --release cargo build --release --features archive
mkdir -p packaging/snap/dist/bin mkdir -p packaging/snap/dist/bin
cp target/release/dcr packaging/snap/dist/bin/ cp target/release/dcr packaging/snap/dist/bin/
cp -r man packaging/snap/dist/man cp -r man packaging/snap/dist/man
@@ -362,7 +368,7 @@ jobs:
cd "\$srcdir/\$pkgname-${PKGVER_ORIG}" cd "\$srcdir/\$pkgname-${PKGVER_ORIG}"
unset CFLAGS CXXFLAGS LDFLAGS unset CFLAGS CXXFLAGS LDFLAGS
export RUSTFLAGS="-C linker=cc" export RUSTFLAGS="-C linker=cc"
cargo build --release --locked cargo build --release --locked --features archive
} }
package() { package() {
cd "\$srcdir/dcr-${PKGVER_ORIG}" cd "\$srcdir/dcr-${PKGVER_ORIG}"
@@ -452,7 +458,7 @@ jobs:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
- name: Build - name: Build
run: cargo build --release --target ${{ matrix.target }} run: cargo build --release --features archive --target ${{ matrix.target }}
- name: Prepare Artifact - name: Prepare Artifact
run: | run: |
@@ -497,7 +503,7 @@ jobs:
run: | run: |
export PATH="/usr/local/bin:/usr/local/sbin:$PATH" export PATH="/usr/local/bin:/usr/local/sbin:$PATH"
pkg install -y rust perl5 gmake pkg install -y rust perl5 gmake
cargo build --release cargo build --release --features archive
- name: Prepare FreeBSD Artifact - name: Prepare FreeBSD Artifact
if: matrix.system == 'freebsd' if: matrix.system == 'freebsd'
@@ -515,7 +521,7 @@ jobs:
run: | run: |
export PATH="/usr/local/bin:$PATH" export PATH="/usr/local/bin:$PATH"
pkg_add rust perl gmake pkg_add rust perl gmake
cargo build --release cargo build --release --features archive
- name: Prepare OpenBSD Artifact - name: Prepare OpenBSD Artifact
if: matrix.system == 'openbsd' if: matrix.system == 'openbsd'
@@ -535,7 +541,7 @@ jobs:
mkdir -p /tmp/bin mkdir -p /tmp/bin
ln -sf /usr/pkg/bin/gmake /tmp/bin/make ln -sf /usr/pkg/bin/gmake /tmp/bin/make
export PATH="/tmp/bin:/usr/pkg/bin:/usr/pkg/sbin:$PATH" export PATH="/tmp/bin:/usr/pkg/bin:/usr/pkg/sbin:$PATH"
cargo build --release cargo build --release --features archive
- name: Prepare NetBSD Artifact - name: Prepare NetBSD Artifact
if: matrix.system == 'netbsd' if: matrix.system == 'netbsd'
@@ -583,7 +589,7 @@ jobs:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
- name: Build - name: Build
run: cargo build --release --target ${{ matrix.target }} run: cargo build --release --features archive --target ${{ matrix.target }}
- name: Prepare Artifact - name: Prepare Artifact
shell: pwsh shell: pwsh
@@ -652,7 +658,7 @@ jobs:
cd "\$srcdir/\$pkgname-${PKGVER_ORIG}" cd "\$srcdir/\$pkgname-${PKGVER_ORIG}"
unset CFLAGS CXXFLAGS LDFLAGS unset CFLAGS CXXFLAGS LDFLAGS
export RUSTFLAGS="-C linker=cc" export RUSTFLAGS="-C linker=cc"
cargo build --release --locked cargo build --release --locked --features archive
} }
package() { package() {
@@ -917,7 +923,7 @@ jobs:
cd "\$srcdir/dcr-${PKGVER_ORIG}" cd "\$srcdir/dcr-${PKGVER_ORIG}"
unset CFLAGS CXXFLAGS LDFLAGS unset CFLAGS CXXFLAGS LDFLAGS
export RUSTFLAGS="-C linker=cc" export RUSTFLAGS="-C linker=cc"
cargo build --release --locked cargo build --release --locked --features archive
} }
package() { package() {
@@ -1185,6 +1191,7 @@ jobs:
- name: Update Formula/dcr.rb - name: Update Formula/dcr.rb
run: | run: |
VERSION="${{ needs.setup.outputs.version }}" VERSION="${{ needs.setup.outputs.version }}"
cat > Formula/dcr.rb <<FORMULA cat > Formula/dcr.rb <<FORMULA
class Dcr < Formula class Dcr < Formula
desc "Cargo-like utility to manage C/C++ projects" desc "Cargo-like utility to manage C/C++ projects"
@@ -1193,37 +1200,36 @@ jobs:
on_macos do on_macos do
on_intel 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 }}" sha256 "${{ steps.sha.outputs.macos_x86 }}"
end end
on_arm do 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 }}" sha256 "${{ steps.sha.outputs.macos_arm }}"
end end
end end
on_linux do on_linux do
on_intel 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 }}" sha256 "${{ steps.sha.outputs.linux_x86 }}"
end end
on_arm do 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 }}" sha256 "${{ steps.sha.outputs.linux_arm }}"
end end
end end
def install
bin.install Dir["dcr*"].first => "dcr"
end
resource "man-pages" do 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 }}" sha256 "${{ steps.sha.outputs.man_pages }}"
end end
def install def install
bin.install Dir["dcr*"].first => "dcr" bin.install Dir["dcr*"].first => "dcr"
resource("man-pages").stage do resource("man-pages").stage do
man1.install Dir["usr/share/man/man1/*.1"] man1.install Dir["usr/share/man/man1/*.1"]
end end
@@ -1252,3 +1258,72 @@ jobs:
# #
# publish-snap: # publish-snap:
# if: ... # 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 }}"

4
.gitignore vendored
View File

@@ -6,3 +6,7 @@ sandbox/**
.env .env
personal/ personal/
.*
!.gitignore
!.github/
graphify-out/

View File

@@ -1,56 +0,0 @@
# AGENTS.md
## Quick start
```bash
cargo build --release && ./target/release/dcr setup
```
## Pre-PR (CI) gate
```bash
cargo fmt --all --check
cargo clippy --all-targets --all-features -- -D warnings
cargo check --all-targets --all-features
cargo test
```
CI (`.github/workflows/ci.yml`) runs fmt, clippy, check — **not** tests.
## Tests
- Integration tests in `tests/` build the dcr binary and run it in temp dirs under `sandbox/cli-tests/` (gitignored).
- Some tests **skip** if no C compiler (`gcc`/`clang`/`cc`) is found at runtime.
- Compiler can be overridden via `DCR_COMPILER` or `DCR_CC` env vars.
- Tests use `$CARGO_BIN_EXE_dcr` when available, else `target/debug/dcr`.
## Architecture
- **Single Rust package** (edition 2024), not a workspace. Only one binary: `dcr`.
- **Entrypoint**: `src/main.rs` — matches `args[1]` against command names.
- **Compiler backends** in `src/core/builder/`: `unix_cc` (gcc/clang), `msvc` (cl/clang-cl), `gas`, `nasm`. Dispatched by matching compiler name string.
- **Config**: `dcr.toml` (per-project), `~/.dcr/config.toml` (registry), `~/.dcr/index.json` (index). Defaults: `language=c`, `standard=c11`, `compiler=clang`, `kind=bin`.
- **`build.rs`** embeds `TARGET` as `DCR_TARGET` env var (shown in `--version`).
- **CLI output** must be in English. Use `utils::log` for messaging.
## Key commands
| Command | Notes |
|---|---|
| `dcr new <name>` | Creates project dir |
| `dcr init` | Init from CWD (must be empty) |
| `dcr build [--debug\|--release] [--target <triple>]` | Default profile: `--debug` |
| `dcr run [--debug\|--release]` | Only for `kind = "bin"` |
| `dcr clean [--debug\|--release] [--all]` | `--all` for workspace roots |
| `dcr tree` | Dependency tree viewer |
| `dcr test [--init]` | Runs/test project tests |
| `dcr gen <vscode\|clion\|compile-commands>` | IDE integration |
| `dcr --update` | Self-update from GitHub Releases |
## Cross-compilation
`--target` accepts short names (`linux`, `macos`, `windows`) or full triples. In CI, Linux cross-builds use `cargo-zigbuild`. When `build.target` is set in `dcr.toml`, DCR auto-injects `--target=<value>` into cflags (clang). Bare-metal targets (containing `none`/`-elf`/`eabi`/`baremetal`) skip DCR's default compiler flags.
## Dependencies (Cargo)
reqwest (blocking+json+rustls-tls), serde, toml, git2, openssl (vendored), self-replace, sha2, ctrlc.

View File

@@ -1,5 +1,377 @@
# Changelog # Changelog
## [v0.9.1] - 2026-09-13 "Системные pkg-config-зависимости / System pkg-config Dependencies"
### RU
**Добавлено:**
- **Зависимости через `pkg-config`** — `fmt = { pkg-config = "fmt" }` или массив имён пакетов (`pkg-config = ["openssl", "libcrypto"]`) подключает системные библиотеки через `.pc`-метаданные.
- **Поддержка префиксов в `dcr add`** — команды `dcr add fmt pkg-config:fmt` и `dcr add openssl pkg:openssl,libcrypto` добавляют зависимости от системных пакетов в `dcr.toml`.
- **Полный набор флагов** — DCR собирает и пробрасывает compiler/linker flags (`-I`, `-L`, `-l`, `-pthread` и другие), включая транзитивные зависимости и интеграцию с `dcr gen` / `dcr test`.
- **Фиксация в `dcr.lock`** — записывается источник `pkg-config+...` и обнаруженная версия пакета.
- **Отображение в `dcr tree`** — `pkg-config` зависимости наглядно помечаются в дереве зависимостей.
**Исправлено:**
- **Ошибка создания проекта в папках со спецсимволами** — `dcr new` больше не завершается ошибкой `Failed to create dcr.toml`, если родительская директория содержит пробелы или не-ASCII символы.
### EN
**Added:**
- **`pkg-config` dependencies** — `fmt = { pkg-config = "fmt" }` or an array of package names (`pkg-config = ["openssl", "libcrypto"]`) connects system libraries through their `.pc` metadata.
- **Prefix support in `dcr add`** — `dcr add fmt pkg-config:fmt` and `dcr add openssl pkg:openssl,libcrypto` add system package dependencies to `dcr.toml`.
- **Complete flag propagation** — compiler/linker flags (`-I`, `-L`, `-l`, `-pthread`, and others) are preserved and passed to builds, `dcr test`, and IDE integrations via `dcr gen`.
- **Lockfile entries** — `dcr.lock` records the `pkg-config+...` source and detected package version.
- **`dcr tree` representation** — `pkg-config` dependencies are clearly labeled in the dependency tree.
**Fixed:**
- **Project creation failure in special character directories** — `dcr new` no longer fails with `Failed to create dcr.toml` when invoked inside parent folders with spaces or non-ASCII characters.
## [v0.9.0] - 2026-08-31 "Полные Path/Git-зависимости и готовые библиотеки / Complete Path/Git Dependencies & Prebuilt Libraries"
### RU
**Добавлено:**
- **Зеркала загрузок в `dcrup`** — `--mirror auto|cf|gh|sf|ya` и `DCRUP_MIRROR`.
По умолчанию `auto`: **cf → gh → sf → ya** (CloudFlare CDN, GitHub, SourceForge, Yandex CDN).
- **Полные Path/Git-зависимости** — DCR-пакеты из `path` и `git` собираются перед потребителем; Git-код кешируется в `~/.dcr/git`, поддерживает `branch`/`tag`/`rev` и фиксирует точный commit в `dcr.lock`.
- **Транзитивные зависимости и защита от циклов** — DCR рекурсивно собирает DCR-пакеты и показывает цепочку циклической зависимости.
- **Внешние готовые библиотеки через path/Git** — `include`, `lib` и `libs` подключают библиотеку без `dcr.toml`; Git prebuilt-источники тоже фиксируются по точному commit.
- **Явный режим зависимости** — `mode = "build"` и `mode = "prebuild"` позволяют выбрать сборку DCR-пакета или подключение готовых артефактов, даже когда репозиторий содержит оба варианта.
**Удалено:**
- **`install.sh` / `install.ps1` / `install_bsd.sh`** — устарели, заменены `dcrup`.
### EN
**Added:**
- **Download mirrors in `dcrup`** — `--mirror auto|cf|gh|sf|ya` and `DCRUP_MIRROR`.
Default `auto` tries **cf → gh → sf → ya** (CloudFlare CDN, GitHub, SourceForge, Yandex CDN).
- **Complete Path/Git dependencies** — DCR packages from `path` and `git` are built before their consumer; Git sources are cached in `~/.dcr/git`, support `branch`/`tag`/`rev`, and lock the exact commit in `dcr.lock`.
- **Transitive dependencies and cycle protection** — DCR recursively builds DCR packages and reports the dependency cycle chain.
- **Prebuilt external libraries through path/Git** — `include`, `lib`, and `libs` link libraries without `dcr.toml`; Git prebuilt sources are locked to their exact commit as well.
- **Explicit dependency mode** — `mode = "build"` and `mode = "prebuild"` select DCR package builds or ready-made artifacts when a repository contains both forms.
**Removed:**
- **`install.sh` / `install.ps1` / `install_bsd.sh`** — superseded by `dcrup`.
## [v0.8.5] - 2026-08-08 "Фиксация версии dcr-version и предупреждения совместимости / Config Version Pinning & Compatibility Warnings"
### RU
**Добавлено:**
- **`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
**Добавлено:**
- **Прогресс компиляции `[N/M]`** — live-строка `compile pkg v0.1.0 [12/41]` на TTY (stderr), чтобы длинные mono-package сборки не выглядели «зависшими».
- **Свой стиль статуса build** — фиксированная колонка глаголов: `project`, `compile`, `dep`, `ready`, `pack`, `done`, `run` (не cargo-clone).
- **Cargo feature `archive`** — `fatfs` и упаковка FAT-образов опциональны (`cargo build --features archive`). Release CI собирает с feature; без feature `[archive]` даёт явную ошибку.
- **Интеграционные тесты** — `flat_bin_nasm_build`, `package_build_target_used_without_cli_flag`, усилен workspace `clean --all`.
- **Расширенный CI** — unit + integration на Linux/macOS/Windows, отдельный job с `--features archive`, rust-cache, clippy default + all-features.
**Изменено:**
- **CLI host triple больше не форсится** — без `--target` используется `build.target` пакета/member (важно для bare-metal / post_steps ISO).
- **Relative `-I`/`-L`/`-T` (и `-isystem`/`-idirafter`)** в cflags/ldflags абсолютизируются относительно корня пакета (workspace CWD = root).
- **`dcr clean --all`** — не спамит `target not found` по members без локального `target/` (общий root `target/`).
- **`error` / `warn`** — красный / жёлтый, вывод на stderr; `Подсказка:` → `Hint:`.
- **`dcr --help` / man** — добавлены `add`, `lint`, `setup`; unknown command → exit code 1.
- **Сообщения NotFound** — `linker not found: ld.lld …` / `{tool} not found …` вместо сырого `os error 2`.
### EN
**Added:**
- **Compile progress `[N/M]`** — live TTY line `compile pkg v0.1.0 [12/41]` so long mono-package builds do not look hung.
- **DCR status verbs** — fixed-width column: `project`, `compile`, `dep`, `ready`, `pack`, `done`, `run`.
- **Cargo feature `archive`** — optional `fatfs` / FAT packing (`cargo build --features archive`). Release CI builds with the feature; without it `[archive]` errors clearly.
- **Integration tests** — `flat_bin_nasm_build`, package `build.target` without CLI flag, stricter workspace `clean --all`.
- **Expanded CI** — unit + integration on Linux/macOS/Windows, archive feature job, rust-cache, clippy default + all-features.
**Changed:**
- **CLI no longer forces host triple** — without `--target`, package/member `build.target` is used (bare-metal / ISO post_steps).
- **Relative `-I`/`-L`/`-T` (and `-isystem`/`-idirafter`)** in flags are absolutized against the package root when workspace CWD is the root.
- **`dcr clean --all`** — no spam for members without a local `target/` (shared root `target/`).
- **`error` / `warn`** — red / yellow on stderr; Russian `Подсказка:` → `Hint:`.
- **`dcr --help` / man** — list `add`, `lint`, `setup`; unknown command exits 1.
- **NotFound messages** — `linker not found: ld.lld …` / `{tool} not found …` instead of raw `os error 2`.
## [0.8.1] - 2026-07-20 "OS-Dev: flat-bin, FAT-образы / OS-Dev: flat-bin & FAT Images"
### RU
**Добавлено:**
- **`build.kind = "flat-bin"`** — сырой бинарный артефакт (расширение по умолчанию `.bin`):
- **NASM** — `-f bin`, прямой вывод `<stem>.bin`
- **FASM** — прямой вывод (ожидается `format binary` в исходнике)
- **GAS / MASM / LLVM IR (`llc`)** — объект → `objcopy -O binary` → `<stem>.bin`
- **C/C++ (gcc/clang/MSVC)** — compile → link (nostdlib/static) → `objcopy -O binary` → `<name>.bin`
- Требуется `llvm-objcopy` / `objcopy` / `gobjcopy` в PATH (кроме NASM/FASM direct)
- **Секция `[archive]`** — после успешной сборки DCR собирает образ диска (FAT12/16/32): размер, offset, volume label, опциональный bootsector и `layout`. С 0.8.2 — за feature `archive`.
- **Одиночные файлы в `build.roots`** — корень может быть файлом (`.c`, `.asm`, header), а не только каталогом.
**Изменено:**
- **`dcr build --force` / `dcr run --force`** — force также перезапускает `build.steps` и `build.post_steps`.
- **`dcr run` на workspace-only root** — если задан `[run].cmd`, сначала build + run по конфигу корня; иначе делегирование члену workspace.
### EN
**Added:**
- **`build.kind = "flat-bin"`** — raw binary (default `.bin`) across NASM/FASM/GAS/MASM/LLC and C/C++ (objcopy where needed).
- **`[archive]` section** — FAT12/16/32 disk images after build (from 0.8.2 gated by feature `archive`).
- **Single-file `build.roots`**.
**Changed:**
- **`--force`** re-runs steps/post_steps; workspace-only **`dcr run`** prefers root `[run].cmd` when set.
## [0.8.0] - 2026-07-18 "Модульный Движок Сборки, Новая Архитектура Ядра и Улучшение Workspaces / Modular Build Engine, New Core Architecture, & Workspace Improvements"
### RU
**Добавлено:**
- **Выделенное ядро сборки (Build Engine extraction)** — вся логика оркестрации компиляции и сборки полностью вынесена из интерфейсного модуля `src/cli/build.rs` в изолированный, переиспользуемый модуль ядра `src/core/build/engine.rs`.
- **Полиморфная модель языков (Трейт `Language`)** — введение абстрактного трейта `Language` (`src/core/build/language/mod.rs`). Логика поиска файлов, сопоставления расширений и компиляторов распределена по выделенным языковым бэкендам для C, C++ (включая шаги генерации Qt), LLVM IR и ассемблера (GAS, NASM, FASM, MASM).
- **Абстракция сборщиков (Трейт `Builder`)** — введение трейта `Builder` для полиморфного управления процессами компиляции (`src/core/build/builder/mod.rs`). Логика вызова компиляторов семейства GCC/Clang вынесена в общий модуль `builder/cc_common.rs`, а MSVC — в `builder/msvc/`.
- **Поязыковая конфигурация (`[build.<lang>]`)** — добавлена поддержка индивидуальных настроек для каждого языка в `dcr.toml` (например, `[build.c]`, `[build.cpp]`, `[build.asm]`), позволяя независимо настраивать компилятор, стандарт и флаги сборки.
- **Интеграция путей зависимостей в Workspaces** — сборщик теперь автоматически инжектирует пути заголовков (включая исходники `src/` членов, папки `include/` и директорию `target/include`) и библиотек (`target/lib`, а также специфичные для целевой платформы папки сборки) зависимых членов workspace при сборке.
- **Система событий `BuildReporter`** — весь вывод сборщика переведён на событийную модель (`src/core/build/report.rs`), что делает движок пригодным для программного встраивания (например, в IDE) без необходимости перехвата stdout/stderr.
- **Поддержка отмены сборки** — добавлен потокобезопасный токен отмены (`Arc<AtomicBool>`), позволяющий безопасно прервать выполнение компиляции на любом этапе.
- **Сохранение кастомных ключей TOML** — парсер `dcr.toml` переведен на `toml_edit`, что гарантирует сохранение всех неизвестных/пользовательских ключей, комментариев и форматирования при перезаписи файла (например, через `dcr add`).
- **Разрешение заголовков для локальных path-зависимостей** — исправлена проблема, при которой потребители не могли найти заголовочные файлы статических/динамических библиотек, лежащие в упакованной папке `target/include` локальных path-зависимостей.
- **Новая система интеграционных тестов** — старый громоздкий файл `tests/cli_basic.rs` разделен на изолированные и структурированные интеграционные тесты для каждой команды и фичи: `cli_build.rs`, `cli_deps.rs`, `cli_lint.rs`, `cli_new.rs`, `cli_qt.rs`, `cli_test.rs`, `cli_workspace.rs`.
**Изменено:**
- **Дедупликация генерации артефактов** — логика линковки бинарных файлов (ELF, PE, EXE), динамических библиотек (`.so`, `.dll`, `.dylib`) и архивации статических библиотек (`.a`, `.lib`) полностью перенесена в выделенный модуль `src/core/build/builder/artifact.rs`.
- **Запуск членов workspace из корня** — команда `dcr run` при вызове из корня workspace-only проекта теперь запускает сборку выбранного пакета в контексте workspace (через флаг `--workspace`), исключая изолированные ошибки сборки.
- **Отказ от глобального состояния** — все операции сборки изолированы внутри структуры `BuildContext`, обеспечивая многопоточную безопасность сборщика.
- **Вынос логики кэширования** — проверка инкрементальных изменений, отслеживание заголовков (`#include`) и вычисление хэшей сборки перенесено в изолированный модуль `src/core/build/cache.rs`.
- **Модуляризация кастомных шагов** — обработка этапа `build.steps` для препроцессоров вынесена в `src/core/build/steps.rs`.
### EN
**Added:**
- **Dedicated Build Engine (`src/core/build/engine.rs`)** — decoupled the build orchestration logic completely from the CLI front-end wrapper in `src/cli/build.rs` to a reusable, decoupled core build engine.
- **Polymorphic Language Model (`Language` trait)** — introduced the abstract `Language` trait (`src/core/build/language/mod.rs`). File scanning, compiler resolution, and flags handling are now encapsulated in dedicated language modules for C, C++ (including Qt code generation), LLVM IR, and ASM (GAS, NASM, FASM, MASM).
- **Polymorphic Compilation Dispatch (`Builder` trait)** — introduced the `Builder` trait to generalize builder invocations (`src/core/build/builder/mod.rs`). Consolidated GCC/Clang logic into `builder/cc_common.rs` and Microsoft Visual C++ logic into `builder/msvc/`.
- **Per-Language Configuration overrides (`[build.<lang>]`)** — introduced support for configuring language-specific compiler, standard, and flags (e.g. `[build.c]`, `[build.cpp]`, `[build.asm]`) independently in `dcr.toml`.
- **Automatic Workspace Dependency Injection** — during workspace builds, include/lib paths of dependent workspace members (including their source headers, local `include/` folders, and build target `target/include` / `target/lib` folders) are automatically resolved and injected.
- **`BuildReporter` Event System** — decoupled build orchestration output into a structured event-driven reporter model (`src/core/build/report.rs`), making DCR suitable for library embedding and IDE integrations without stderr capturing.
- **Build Cancellation Support** — introduced thread-safe cancellation tokens (`Arc<AtomicBool>`) allowing clients to safely abort compile runs mid-execution.
- **TOML Formatting & Custom Keys Preservation** — migrated the config editor to `toml_edit`, preserving all unknown/user-defined TOML keys, structures, and comments during file write operations (e.g., in `dcr add`).
- **Path Dependencies Target Include Resolution** — local path dependencies configured via tables now correctly expose headers built and packaged into their `target/include` folder to consumer packages.
- **Modular Integration Test Suite** — refactored the monolith `tests/cli_basic.rs` file into clean, specialized integration tests: `cli_build.rs`, `cli_deps.rs`, `cli_lint.rs`, `cli_new.rs`, `cli_qt.rs`, `cli_test.rs`, `cli_workspace.rs`.
**Changed:**
- **Consolidated Linking and Archiving** — artifact generation (linking executables/shared libraries and archiving static libraries) consolidated into a single `src/core/build/builder/artifact.rs` module.
- **Workspace-Aware member execution** — `dcr run` executed at the workspace root of a `workspace_only` project now triggers the member build inside the workspace context using the `--workspace` parameter, preventing standalone build issues.
- **Elimination of Global State** — removed global variables and states from the build core, encapsulating build logic inside isolated `BuildContext` structs for thread safety.
- **Build Cache Separation** — relocated mtime tracking, incremental caching, and header-dependency checking logic to `src/core/build/cache.rs`.
- **Modularized Pre-build Steps** — decoupled generator scripts and pre-build commands parsing into `src/core/build/steps.rs`.
## [0.7.4] - 2026-06-17 "Qt Поддержка, Linting и Рефакторинг Архитектуры / Qt Support, Linting, & Architecture Refactor"
### RU
**Добавлено:**
- **Нативная поддержка Qt** — добавлена автоматическая обработка мета-объектов (MOC, UIC, RCC) при включении `build.qt = true` в `dcr.toml`.
- **Команда `dcr lint`** — статический анализ C/C++ через `clang-tidy`. Поддерживает `--fix` для автоматического исправления. Уважает `build.roots` и `build.src_disable` из конфига.
- **Три новых ассемблерных бэкенда**:
- **MASM** (`compiler = "ml"` / `"ml64"`) — Microsoft Macro Assembler.
- **FASM** (`compiler = "fasm"`) — Flat Assembler.
- **LLVM IR** (`compiler = "llc"`) — компиляция `.ll` → объектный файл.
- **Общий ASM-пайплайн** — линковка и архивирование вынесены в `core/builder/asm.rs`. Новый ассемблерный бэкенд теперь пишется за ~30 строк.
- **Утилиты для файловой системы** — `to_hex()` и `home_dir()` вынесены в `utils/fs.rs`.
- **Утилиты для сборки** — `normalize_target()`, `normalize_kind()`, `normalize_platform()`, `default_profile_flags()` вынесены в `utils/build.rs`.
- **Утилиты для бэкендов** — `asm_lang_flag()`, `source_extensions()`, `elapsed_secs()` добавлены в `core/builder/common.rs`.
- **Полноценная автоматизация Bare-Metal / Freestanding** — добавлен конфигурационный флаг `build.freestanding = true`. При его включении (или при обнаружении bare-metal таргета) DCR автоматически инжектирует флаг `-ffreestanding` на этапе компиляции, а также `-nostdlib` и `-static` на этапе линковки.
- **Оптимизация артефактов (LTO и Strip)** — в `dcr.toml` добавлены параметры `build.lto` (авто-инжекция флага `-flto` для компилятора и линкера) и `build.strip` (автоматическое отсекание отладочной информации через флаг `-s` линкера).
- **Контроль многопоточности сборки** — добавлен параметр `build.codegen-units`, позволяющий жестко ограничить максимальное количество параллельных потоков сборки для кастомного движка `parallel_build`.
- **Управление паникой (Panic Abort)** — добавлен параметр `build.panic = "abort"`. Для C++ проектов он автоматически отключает генерацию таблиц раскрутки стека и исключений через флаги `-fno-exceptions`, `-fno-unwind-tables` и `-fno-asynchronous-unwind-tables`.
**Изменено:**
- **Интеллектуальные дефолтные флаги (DCR Defaults)** — генерация дефолтных флагов оптимизации (`-O3`/`-O0`), отладки (`-g`) и предупреждений (`-Wall -Wextra`) теперь автоматически отключается для bare-metal и freestanding целей, если пользователь переопределил `build.cflags`.
- **Глобальный вынос bare-metal детектора** — функция `is_bare_metal_target` вынесена в общие утилиты `src/utils/build.rs` для сквозного использования в конвейере сборки.
- **Рефакторинг архитектуры**:
- `src/config.rs` переименован в `src/templates.rs`.
- Начата консолидация конфигурационной логики и оркестрации сборки.
### EN
**Added:**
- **Native Qt support** — automatic meta-object handling (MOC, UIC, RCC) added when `build.qt = true` is set in `dcr.toml`.
- **`dcr lint` command** — C/C++ static analysis via `clang-tidy`. Supports `--fix` for automatic fixes. Respects `build.roots` and `build.src_disable` from config.
- **Three new assembler backends**:
- **MASM** (`compiler = "ml"` / `"ml64"`) — Microsoft Macro Assembler.
- **FASM** (`compiler = "fasm"`) — Flat Assembler.
- **LLVM IR** (`compiler = "llc"`) — compiles `.ll` files via `llc -filetype=obj`.
- **Shared ASM pipeline** — linking and archiving logic consolidated into `core/builder/asm.rs`. Adding a new assembler backend now takes ~30 lines.
- **Filesystem utilities** — `to_hex()` and `home_dir()` extracted to `utils/fs.rs`.
- **Build utilities** — `normalize_target()`, `normalize_kind()`, `normalize_platform()`, `default_profile_flags()` extracted to `utils/build.rs`.
- **Backend helpers** — `asm_lang_flag()`, `source_extensions()`, `elapsed_secs()` added to `core/builder/common.rs`.
- **Full Bare-Metal / Freestanding Automation** — introduced the `build.freestanding = true` configuration option. When enabled (or when a bare-metal target is detected), DCR automatically injects `-ffreestanding` during compilation and both `-nostdlib` and `-static` during linking.
- **Artifact Optimization (LTO & Strip)** — added `build.lto` (auto-injects `-flto` for compiler and linker) and `build.strip` (automatically strips debug symbols via linker `-s` flag) options to `dcr.toml`.
- **Compilation Thread Control** — added the `build.codegen-units` option to strictly limit the maximum number of parallel jobs utilized by the custom `parallel_build` worker pool.
- **Panic Behavior Management** — added `build.panic = "abort"` support. For C++ targets, it automatically strips exception handling and unwind tables via `-fno-exceptions`, `-fno-unwind-tables`, and `-fno-asynchronous-unwind-tables`.
**Changed:**
- **Intelligent Default Flags Generation** — automated injection of fallback optimization (`-O3`/`-O0`), debug (`-g`), and warning (`-Wall -Wextra`) flags is now suppressed for bare-metal and freestanding builds if `build.cflags` are overridden.
- **Centralized Bare-Metal Detection** — relocated the `is_bare_metal_target` helper to common build utilities (`src/utils/build.rs`) for unified access across the building core.
- **Architectural Refactoring**:
- Renamed `src/config.rs` to `src/templates.rs`.
- Started consolidation of configuration and build orchestration logic.
## [0.7.3] - 2026-06-13 "Оптимизация Зависимостей и VCS / Dependency Optimization & VCS" ## [0.7.3] - 2026-06-13 "Оптимизация Зависимостей и VCS / Dependency Optimization & VCS"
### RU ### RU
@@ -37,25 +409,31 @@
### RU ### RU
**Добавлено:** **Добавлено:**
- Учтён `target_env` в `default_target_triple()` — Linux (gnu/musl) и Windows (msvc/gnu) теперь используют правильное окружение, а не хардкод. - Учтён `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`. - **Сборка на 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. - **Бейджи GitHub Stars и GPL-3.0 не рендерились в README** — блок бейджей переведён на чистый HTML.
**Изменено:** **Изменено:**
- Вычисление таргета по умолчанию вынесено в общую функцию `default_target_triple()` — устранено дублирование в build, run, clean. - Вычисление таргета по умолчанию вынесено в общую функцию `default_target_triple()` — устранено дублирование в build, run, clean.
### EN ### EN
**Added:** **Added:**
- `default_target_triple()` now respects `target_env` — Linux (gnu/musl) and Windows (msvc/gnu) use the correct environment instead of hardcoded values. - `default_target_triple()` now respects `target_env` — Linux (gnu/musl) and Windows (msvc/gnu) use the correct environment instead of hardcoded values.
**Fixed:** **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`. - **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. - **GitHub Stars and GPL-3.0 badges not rendering in README** — badge block converted to pure HTML.
**Changed:** **Changed:**
- Default target resolution extracted into shared `default_target_triple()` — removed duplication across build, run, clean. - 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" ## [0.7.1] - 2026-06-02 "Мультиархитектурное расширение / The Multi-Arch Expansion"
@@ -63,6 +441,7 @@
### RU ### RU
**Добавлено:** **Добавлено:**
- Вывод `--help` для всех команд с цветным форматированием. - Вывод `--help` для всех команд с цветным форматированием.
- man-страницы (12 штук в `man/man1/`) и их установка во всех пакетах. - man-страницы (12 штук в `man/man1/`) и их установка во всех пакетах.
- Валидация имен проектов в `dcr init` и `dcr new` (только ASCII буквы, цифры, `_` и `-`). - Валидация имен проектов в `dcr init` и `dcr new` (только ASCII буквы, цифры, `_` и `-`).
@@ -73,6 +452,7 @@
- Пакеты AUR (`dcr-dev`, `dcr-dev-bin`) и сборка Snap в CI. - Пакеты AUR (`dcr-dev`, `dcr-dev-bin`) и сборка Snap в CI.
**Изменено:** **Изменено:**
- Функция `validate_package_name` стала публичной. - Функция `validate_package_name` стала публичной.
- Команды `gen`, `tree`, `fmt`, `setup` больше не игнорируют аргументы и используют цветной вывод. - Команды `gen`, `tree`, `fmt`, `setup` больше не игнорируют аргументы и используют цветной вывод.
- Инструкция в README изменена с `| bash` на `| sh` для POSIX. - Инструкция в README изменена с `| bash` на `| sh` для POSIX.
@@ -90,6 +470,7 @@
### EN ### EN
**Added:** **Added:**
- `--help` for all commands with styled headers and usage lines. - `--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. - 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). - Project name validation in `dcr init` and `dcr new` (ASCII letters, digits, `_`, `-` only).
@@ -100,6 +481,7 @@
- AUR packages (`dcr-dev`, `dcr-dev-bin`) and Snap build in CI. - AUR packages (`dcr-dev`, `dcr-dev-bin`) and Snap build in CI.
**Changed:** **Changed:**
- `validate_package_name` made public and callable from CLI commands directly. - `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. - `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. - README install instructions changed from `| bash` to `| sh` for POSIX compatibility.
@@ -276,6 +658,7 @@ Added:
``` ```
Produces `KERNEL.EFI` (works for `bin`, `staticlib`, and `sharedlib`). 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`). - 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`. - 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. - `build.ldscript` in `dcr.toml` — linker script path passed as `-T <path>` to the linker. Essential for bare-metal/embedded/freestanding targets.

View File

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

319
Cargo.lock generated
View File

@@ -2,18 +2,39 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.102" version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.22.1" version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.11.1" version = "2.11.1"
@@ -38,6 +59,28 @@ dependencies = [
"objc2", "objc2",
] ]
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cc"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.4" version = "1.0.4"
@@ -50,6 +93,19 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@@ -98,15 +154,17 @@ dependencies = [
[[package]] [[package]]
name = "dcr" name = "dcr"
version = "0.7.3" version = "0.9.1"
dependencies = [ dependencies = [
"ctrlc", "ctrlc",
"fatfs",
"glob", "glob",
"self-replace", "self-replace",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
"toml", "toml",
"toml_edit",
"ureq", "ureq",
] ]
@@ -126,7 +184,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.11.1",
"block2", "block2",
"libc", "libc",
"objc2", "objc2",
@@ -165,6 +223,24 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
[[package]]
name = "fatfs"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05669f8e7e2d7badc545c513710f0eba09c2fbef683eb859fd79c46c355048e0"
dependencies = [
"bitflags 1.3.2",
"byteorder",
"chrono",
"log",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]] [[package]]
name = "foldhash" name = "foldhash"
version = "0.1.5" version = "0.1.5"
@@ -180,6 +256,30 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "futures-core"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
[[package]]
name = "futures-task"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
[[package]]
name = "futures-util"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.7" version = "0.14.7"
@@ -230,6 +330,30 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "2.2.0" version = "2.2.0"
@@ -357,6 +481,17 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
dependencies = [
"cfg-if",
"futures-util",
"wasm-bindgen",
]
[[package]] [[package]]
name = "leb128fmt" name = "leb128fmt"
version = "0.1.0" version = "0.1.0"
@@ -399,12 +534,21 @@ version = "0.31.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.11.1",
"cfg-if", "cfg-if",
"cfg_aliases", "cfg_aliases",
"libc", "libc",
] ]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "objc2" name = "objc2"
version = "0.6.4" version = "0.6.4"
@@ -438,6 +582,12 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.5" version = "0.1.5"
@@ -487,7 +637,7 @@ version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.11.1",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
@@ -525,6 +675,12 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]] [[package]]
name = "schannel" name = "schannel"
version = "0.1.29" version = "0.1.29"
@@ -540,7 +696,7 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.11.1",
"core-foundation", "core-foundation",
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@@ -637,6 +793,18 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.15.1" version = "1.15.1"
@@ -703,12 +871,18 @@ dependencies = [
"indexmap", "indexmap",
"serde_core", "serde_core",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime 1.1.1+spec-1.1.0",
"toml_parser", "toml_parser",
"toml_writer", "toml_writer",
"winnow", "winnow 1.0.3",
] ]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "1.1.1+spec-1.1.0" version = "1.1.1+spec-1.1.0"
@@ -718,15 +892,33 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"toml_datetime 0.6.11",
"toml_write",
"winnow 0.7.15",
]
[[package]] [[package]]
name = "toml_parser" name = "toml_parser"
version = "1.1.2+spec-1.1.0" version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [ dependencies = [
"winnow", "winnow 1.0.3",
] ]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]] [[package]]
name = "toml_writer" name = "toml_writer"
version = "1.1.1+spec-1.1.0" version = "1.1.1+spec-1.1.0"
@@ -808,6 +1000,51 @@ dependencies = [
"wit-bindgen 0.51.0", "wit-bindgen 0.51.0",
] ]
[[package]]
name = "wasm-bindgen"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
dependencies = [
"unicode-ident",
]
[[package]] [[package]]
name = "wasm-encoder" name = "wasm-encoder"
version = "0.244.0" version = "0.244.0"
@@ -836,18 +1073,71 @@ version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.11.1",
"hashbrown 0.15.5", "hashbrown 0.15.5",
"indexmap", "indexmap",
"semver", "semver",
] ]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.2.1" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"
@@ -930,6 +1220,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "1.0.3" version = "1.0.3"
@@ -1000,7 +1299,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags", "bitflags 2.11.1",
"indexmap", "indexmap",
"log", "log",
"serde", "serde",

View File

@@ -1,13 +1,13 @@
[package] [package]
name = "dcr" name = "dcr"
version = "0.7.3" version = "0.9.1"
edition = "2024" edition = "2024"
build = "build.rs" build = "build.rs"
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style." description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
repository = "https://github.com/dexoron/dcr" repository = "https://github.com/dexoron/dcr"
documentation = "https://dcr.dexoron.su/docs" documentation = "https://dcr-tool.ru/docs"
homepage = "https://dcr.dexoron.su" homepage = "https://dcr-tool.ru"
[package.metadata.binstall] [package.metadata.binstall]
pkg-fmt = "bin" pkg-fmt = "bin"
@@ -59,6 +59,10 @@ assets = [
{ source = "man/man1/dcr-tree.1", dest = "/usr/share/man/man1/dcr-tree.1", mode = "644" }, { source = "man/man1/dcr-tree.1", dest = "/usr/share/man/man1/dcr-tree.1", mode = "644" },
] ]
[features]
default = []
archive = ["dep:fatfs"]
[dependencies] [dependencies]
glob = "0.3" glob = "0.3"
ureq = { version = "2.10", default-features = false, features = ["json", "native-certs"] } ureq = { version = "2.10", default-features = false, features = ["json", "native-certs"] }
@@ -66,8 +70,10 @@ serde = { version = "1", features = ["derive"] }
self-replace = "1" self-replace = "1"
sha2 = "0.10" sha2 = "0.10"
toml = "1.0.3" toml = "1.0.3"
toml_edit = "0.22"
serde_json = "1" serde_json = "1"
ctrlc = "3" ctrlc = "3"
fatfs = { version = "0.3", optional = true }
[profile.release] [profile.release]
opt-level = "z" opt-level = "z"
@@ -83,3 +89,12 @@ lto = false
codegen-units = 16 codegen-units = 16
panic = "abort" panic = "abort"
strip = true 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: Recommended path:
- check instructions in `README.md`; - 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`. - 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? ## How can I get started quickly?
Minimal workflow: Minimal workflow:

View File

@@ -34,38 +34,15 @@
<table> <table>
<tr> <tr>
<td><b>Linux (Dexoron Packages)</b></td>
<td>
See https://dcr.dexoron.su for settings youre packages manager
**Arch Linux:**
```sh
sudo pacman -Sy dcr
```
**Debian/Ubuntu:**
```sh
sudo apt update && sudo apt install dcr
```
**Fedora/RHEL:**
```sh
sudo dnf install dcr
```
</td>
</tr>
<tr>
<td><b>Arch Linux (AUR)</b></td> <td><b>Arch Linux (AUR)</b></td>
<td> <td>
```sh ```sh
yay -S dcr 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> </td>
</tr> </tr>
@@ -103,21 +80,32 @@ cargo install dcr
</td> </td>
</tr> </tr>
<tr> <tr>
<td><b>Linux / macOS (script)</b></td> <td><b>dcrup (Linux / macOS / BSD / Windows(Bash))</b></td>
<td> <td>
```sh ```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> </td>
</tr> </tr>
<tr> <tr>
<td><b>BSD (script)</b></td> <td><b>dcrup (Windows PowerShell)</b></td>
<td> <td>
```sh ```powershell
curl -fsSL https://dcr.dexoron.su/install_bsd.sh | sh 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> </td>
@@ -133,16 +121,6 @@ curl -fsSL https://dcr.dexoron.su/install_bsd.sh | sh
sudo snap install --classic --dangerous dcrup_*.snap 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> </td>
</tr> </tr>
<tr> <tr>
@@ -151,7 +129,7 @@ irm https://dcr.dexoron.su/install.ps1 | iex
```sh ```sh
git clone https://github.com/dexoron/dcr.git 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 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 ## Contributors
<div align="center"> <div align="center">

69
TODO.md
View File

@@ -1,69 +0,0 @@
# DCR TODO.md
Priority list of tasks to turn DCR into a true Cargo killer for C/C++ developers.
## 🔥 High Priority (Next Releases)
- [x] **Git dependencies support**
`git = "https://github.com/user/repo.git#branch"` and `git = "..."@tag`
- [ ] **Conan and vcpkg integration**
Automatic download, installation and linking of packages
- [ ] **Full freestanding / embedded support**
`-nostdlib`, linker scripts, bare-metal targets (i686-elf, aarch64-none-elf, etc.)
- [ ] **EFI support**
Dedicated platform configuration for EFI targets
- [ ] **Custom project name**
Override binary name in `dcr.toml`
- [ ] **Custom build steps / targets**
Ability to define custom pre-build, post-build, codegen steps in `dcr.toml`
- [ ] **Improved mixed C/C++/ASM projects**
Better handling of `.S` and `.asm` files with correct flags
- [ ] **dcr publish + official package registry**
Publish and fetch your own libraries easily
## ✅ Medium Priority
- [ ] **Static analysis** (`dcr check`)
Integration with clang-tidy, cppcheck, include-what-you-use
- [ ] **Linter + Formatter** (`dcr fmt`, `dcr lint`)
- [ ] **Hot reload / Live reload** for applications (raylib, SFML, SDL, etc.)
- [ ] **Better cross-compilation experience**
Ready-to-use toolchains + automatic download
- [ ] **Linker script support** + direct linker control
- [ ] **Workspaces improvements**
Per-member include directories, selective builds, better dependency graph
- [ ] **Build-time code generation**
Support for protobuf, flatbuffers, Qt moc, custom generators
## 📌 Low Priority / Nice to Have
- [ ] Support for Meson and Ninja as alternative backends
- [ ] Dockerfile generation (`dcr docker init`)
- [ ] First-class Windows MSVC support (currently works but rough)
- [ ] Documentation generation (`dcr doc`)
- [ ] Sanitizers integration (`dcr sanitize`)
- [ ] Benchmarks and comparison with CMake/Meson
- [ ] C++20 modules support (when it becomes stable)
## 🧠 Future Ideas
- Official DCR package registry (dcrhub)
- GUI wrapper (dcr-gui)
- Plugin system
- Toolchain manager (like rustup)
- Zig as alternative compiler
- deb/rpm/flatpak/AppImage packaging support
---
**Currently in progress:**
- ...
**Completed in recent releases:**
- Workspaces
- Cross-compilation
- IDE config generation (VS Code, CLion, compile_commands.json)
- Self-update (`dcr --update`)
- Header tracking

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%

13
dcrup.cmd Executable file
View File

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

434
dcrup.ps1 Normal file
View File

@@ -0,0 +1,434 @@
#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' }
$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 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
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 {
Die 'use: dcrup uninstall --all (or install a replacement first)'
}
}
'self-install' { Cmd-SelfInstall }
default { Die "unknown command: $cmd" }
}

668
dcrup.sh Executable file
View File

@@ -0,0 +1,668 @@
#!/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_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"
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 || echo "$0")"
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> <summary>Quick reference</summary>
```bash ```bash
# Installation # Installation (dcrup)
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 / GitHub / SourceForge / Yandex
# Create a project # Create a project
dcr new my-app dcr new my-app
@@ -51,8 +54,9 @@ dcr gen vscode
- **Incremental builds** — mtime + `.d` headers + SHA256 fingerprint - **Incremental builds** — mtime + `.d` headers + SHA256 fingerprint
- **Parallel compilation** — thread::scope, atomic task queue - **Parallel compilation** — thread::scope, atomic task queue
- **4 backends** — gcc/clang, MSVC/clang-cl, GAS (ARM), NASM (x86) - **7 backends** — gcc/clang (unix_cc), MSVC/clang-cl (msvc), GAS, NASM, MASM, FASM, LLVM IR
- **7 project kinds** — bin, staticlib, sharedlib, efi, elf, none, custom - **8 project kinds** — bin, staticlib, sharedlib, efi, elf, none, custom, flat-bin
- **Disk images** — optional `[archive]` packs FAT12/16/32 images after build
- **Cross-compilation** — short names and full triples - **Cross-compilation** — short names and full triples
- **Profiles** — debug / release with field overrides - **Profiles** — debug / release with field overrides
- **Workspaces** — multi-package projects with topological sort - **Workspaces** — multi-package projects with topological sort

View File

@@ -4,6 +4,202 @@ sidebar_label: Changelog
# Changelog # Changelog
## 0.9.1 (2026-09-13)
- **`pkg-config` dependencies** — declare `fmt = { pkg-config = "fmt" }` or multiple package names in `[dependencies]`; DCR applies `.pc` compiler/linker flags and records the resolved package in `dcr.lock`.
- **`dcr add` support for `pkg-config`** — easily add system package dependencies with `dcr add fmt pkg-config:fmt` or `dcr add openssl pkg:openssl,libcrypto`.
- **`dcr tree` & IDE integration** — `pkg-config` packages are marked in `dcr tree` and resolved into compiler flags in `dcr gen` (`compile_commands.json`).
- **Fixed `dcr new` in non-ASCII / spaced paths** — initializing projects from folders with non-ASCII characters or spaces no longer fails validation.
## 0.9.0 (2026-08-31)
- **Download mirrors in `dcrup`** — `--mirror auto|cf|gh|sf|ya` and `DCRUP_MIRROR`.
Default `auto` tries **cf → gh → sf → ya** (CloudFlare CDN, GitHub, SourceForge, Yandex CDN).
- **Complete Path/Git dependencies** — DCR packages from `path` and `git` are built before their consumer. Git sources are cached in `~/.dcr/git`, support `branch`/`tag`/`rev`, and record the exact commit in `dcr.lock`.
- **Build and prebuild modes** — `path` and `git` dependencies can build a DCR package from `dcr.toml` or link existing `include`/`lib`/`libs` artifacts; `mode = "build"` / `"prebuild"` selects explicitly when both forms are present.
- **Prebuilt dependency examples** — documentation now shows the `dcr-math-lib` Git repository and recommended build/prebuilt layouts for both `git` and `path`.
- **Transitive dependency builds and cycle reporting** — DCR recursively builds DCR packages and reports a clear dependency chain for cycles.
- **Bare path arguments for `dcr add`** — `dcr add mylib ../mylib` now works in addition to the explicit `path:` form.
- **Removed `install.sh` / `install.ps1` / `install_bsd.sh`** — superseded by `dcrup`.
## 0.8.5 (2026-08-08)
- **`package.dcr-version`** — optional CLI version pin in `dcr.toml`.
`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
- **Compile progress `[N/M]`** — live TTY status line (`compile pkg v0.1.0 [12/41]`) so long mono-package builds do not look hung.
- **DCR status style** — fixed-width verbs: `project`, `compile`, `dep`, `ready`, `pack`, `done`, `run`.
- **Cargo feature `archive`** — optional FAT packing via `fatfs` (`cargo build --features archive`). Release CI builds with the feature; without it `[archive]` fails with a clear message.
- **Tests** — `flat_bin_nasm_build`, package `build.target` without CLI `--target`, stricter workspace `clean --all`.
- **CI expansion** — unit + integration on Linux/macOS/Windows, archive feature job, rust-cache, clippy default + all-features (see `.github/workflows/ci.yml`).
### Changed
- **No forced host triple from CLI** — without `--target`, package/member `build.target` is honored (bare-metal / ISO `post_steps` paths).
- **Relative include/lib flags** (`-I`, `-i`, `-isystem`, `-idirafter`, `-L`, `-T`) absolutized against the package root under workspace builds.
- **`dcr clean --all`** — no spam for members without a local `target/` (shared root `target/`).
- **`error` / `warn`** — red / yellow on stderr; CLI fully English (`Hint:`).
- **Help / man** — list `add`, `lint`, `setup`; unknown command exits with code 1.
- **Tool NotFound** — `linker not found: ld.lld …` / `{tool} not found …` instead of raw `os error 2`.
## 0.8.1 (2026-07-20)
### Added
- **`build.kind = "flat-bin"`** — raw binary (default `.bin`) for OS-dev payloads:
- **NASM** — `-f bin` → `<stem>.bin`
- **FASM** — direct output (`format binary` in source)
- **GAS / MASM / LLC** — object → `objcopy -O binary` → `<stem>.bin`
- **C/C++** — objects → freestanding link → `objcopy -O binary` → `<name>.bin`
- Requires `llvm-objcopy` / `objcopy` / `gobjcopy` except for NASM/FASM direct emit
- **`[archive]` section** — FAT12/16/32 disk images after build (`output`, `format`, `size`, `offset`, `label`, `bootsector`, `layout`). From 0.8.2 requires feature `archive`.
- **Single-file `build.roots`** — a root may be a single source or header file, not only a directory.
### Changed
- **`--force`** also re-runs `build.steps` and `build.post_steps`.
- **`dcr run` on workspace-only root** — if `[run].cmd` is set, run that after build; otherwise delegate to a workspace member.
## 0.8.0 (2026-07-18)
### Added
- **Dedicated Build Engine (`src/core/build/engine.rs`)** — decoupled the build orchestration logic completely from the CLI front-end wrapper in `src/cli/build.rs` to a reusable, decoupled core build engine.
- **Polymorphic Language Model (`Language` trait)** — introduced the abstract `Language` trait (`src/core/build/language/mod.rs`). File scanning, compiler resolution, and flags handling are now encapsulated in dedicated language modules for C, C++ (including Qt code generation), LLVM IR, and ASM (GAS, NASM, FASM, MASM).
- **Polymorphic Compilation Dispatch (`Builder` trait)** — introduced the `Builder` trait to generalize builder invocations (`src/core/build/builder/mod.rs`). Consolidated GCC/Clang logic into `builder/cc_common.rs` and Microsoft Visual C++ logic into `builder/msvc/`.
- **Per-Language Configuration overrides (`[build.<lang>]`)** — introduced support for configuring language-specific compiler, standard, and flags (e.g. `[build.c]`, `[build.cxx]`, `[build.asm]`) independently in `dcr.toml`.
- **Automatic Workspace Dependency Injection** — during workspace builds, include/lib paths of dependent workspace members (including their source headers, local `include/` folders, and build target `target/include` / `target/lib` folders) are automatically resolved and injected.
- **`BuildReporter` Event System** — decoupled build orchestration output into a structured event-driven reporter model (`src/core/build/report.rs`), making DCR suitable for library embedding and IDE integrations without stderr capturing.
- **Build Cancellation Support** — introduced thread-safe cancellation tokens (`Arc<AtomicBool>`) allowing clients to safely abort compile runs mid-execution.
- **TOML Formatting & Custom Keys Preservation** — migrated the config editor to `toml_edit`, preserving all unknown/user-defined TOML keys, structures, and comments during file write operations (e.g., in `dcr add`).
- **Path Dependencies Target Include Resolution** — local path dependencies configured via tables now correctly expose headers built and packaged into their `target/include` folder to consumer packages.
- **Modular Integration Test Suite** — refactored the monolith `tests/cli_basic.rs` file into clean, specialized integration tests: `cli_build.rs`, `cli_deps.rs`, `cli_lint.rs`, `cli_new.rs`, `cli_qt.rs`, `cli_test.rs`, `cli_workspace.rs`.
### Changed
- **Consolidated Linking and Archiving** — artifact generation (linking executables/shared libraries and archiving static libraries) consolidated into a single `src/core/build/builder/artifact.rs` module.
- **Workspace-Aware member execution** — `dcr run` executed at the workspace root of a `workspace_only` project now triggers the member build inside the workspace context using the `--workspace` parameter, preventing standalone build issues.
- **Elimination of Global State** — removed global variables and states from the build core, encapsulating build logic inside isolated `BuildContext` structs for thread safety.
- **Build Cache Separation** — relocated mtime tracking, incremental caching, and header-dependency checking logic to `src/core/build/cache.rs`.
- **Modularized Pre-build Steps** — decoupled generator scripts and pre-build commands parsing into `src/core/build/steps.rs`.
## 0.7.4 (2026-06-17)
### Added
- **Native Qt support** — automatic meta-object handling (MOC, UIC, RCC) added when `build.qt = true` is set in `dcr.toml`.
- **`dcr lint` command** — C/C++ static analysis via `clang-tidy`. Supports `--fix` for automatic fixes. Respects `build.roots` and `build.src_disable` from config.
- **Three new assembler backends**:
- **MASM** (`compiler = "ml"` / `"ml64"`) — Microsoft Macro Assembler.
- **FASM** (`compiler = "fasm"`) — Flat Assembler.
- **LLVM IR** (`compiler = "llc"`) — compiles `.ll` files via `llc -filetype=obj`.
- **Shared ASM pipeline** — linking and archiving logic consolidated into `core/builder/asm.rs`. Adding a new assembler backend now takes ~30 lines.
- **Filesystem utilities** — `to_hex()` and `home_dir()` extracted to `utils/fs.rs`.
- **Build utilities** — `normalize_target()`, `normalize_kind()`, `normalize_platform()`, `default_profile_flags()` extracted to `utils/build.rs`.
- **Backend helpers** — `asm_lang_flag()`, `source_extensions()`, `elapsed_secs()` added to `core/builder/common.rs`.
- **Full Bare-Metal / Freestanding Automation** — introduced the `build.freestanding = true` configuration option. When enabled (or when a bare-metal target is detected), DCR automatically injects `-ffreestanding` during compilation and both `-nostdlib` and `-static` during linking.
- **Artifact Optimization (LTO & Strip)** — added `build.lto` (auto-injects `-flto` for compiler and linker) and `build.strip` (automatically strips debug symbols via linker `-s` flag) options to `dcr.toml`.
- **Compilation Thread Control** — added the `build.codegen-units` option to strictly limit the maximum number of parallel jobs utilized by the custom `parallel_build` worker pool.
- **Panic Behavior Management** — added `build.panic = "abort"` support. For C++ targets, it automatically strips exception handling and unwind tables via `-fno-exceptions`, `-fno-unwind-tables`, and `-fno-asynchronous-unwind-tables`.
### Changed
- **Intelligent Default Flags Generation** — automated injection of fallback optimization (`-O3`/`-O0`), debug (`-g`), and warning (`-Wall -Wextra`) flags is now suppressed for bare-metal and freestanding builds if `build.cflags` are overridden.
- **Centralized Bare-Metal Detection** — relocated the `is_bare_metal_target` helper to common build utilities (`src/utils/build.rs`) for unified access across the building core.
- **Architectural Refactoring**:
- Renamed `src/config.rs` to `src/templates.rs`.
- Started consolidation of configuration and build orchestration logic.
## 0.7.3 (2026-06-13)
### Added
- **`--vcs` option for `new` and `init` commands** — introduced explicit version control system selection via `--vcs <git|none>`.
- **Git metadata integration in `--version`** — the CLI now appends the current short commit hash and a `-dirty` suffix if there are uncommitted changes in the DCR repository.
- **Automatic `.gitignore` generation** — Git repository initialization now automatically writes a `.gitignore` file excluding the `/target` directory.
- **Nested repository prevention** — automatic Git initialization is now skipped if `dcr new` or `dcr init` is executed inside an already existing Git repository.
- **VCS verification tests** — added `new_vcs_options_work` and `init_vcs_options_work` integration tests to ensure reliable repository behavior.
### Changed
- **Complete removal of `git2` dependency** — all Git actions (dependency fetching, VCS setup, status checks) are now delegated to the system `git` executable via `Command`. This simplifies compilation and drops the need for `vendored` and `openssl` features.
- **Migrated from `reqwest` to `ureq`** — replaced the heavy `reqwest` crate in `flag_update.rs` with the lightweight synchronous `ureq 2.10` client, reducing overhead and the final binary size.
## 0.7.2 (2026-06-07)
### 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) ## 0.7.1 (2026-06-02)
### Added ### Added

View File

@@ -13,7 +13,7 @@ dcr build # debug profile
dcr build --release # release build dcr build --release # release build
dcr build --debug # explicit debug dcr build --debug # explicit debug
dcr build --target aarch64-unknown-linux-gnu dcr build --target aarch64-unknown-linux-gnu
dcr build --force # full rebuild dcr build --force # full rebuild (also re-runs build.steps / post_steps)
dcr build --clean # clean + build dcr build --clean # clean + build
dcr build --verbose # show compilation commands dcr build --verbose # show compilation commands
dcr build --workspace pkg-a # build specific workspace package dcr build --workspace pkg-a # build specific workspace package
@@ -21,6 +21,8 @@ dcr build --workspace pkg-a # build specific workspace package
Artifacts: `target/<triple>/<profile>/<binary-name>` (Linux with explicit target). Artifacts: `target/<triple>/<profile>/<binary-name>` (Linux with explicit target).
With `[archive]` in `dcr.toml`, a FAT disk image is packed after a successful build.
## `dcr run` ## `dcr run`
Builds (if sources are newer than artifacts) and runs the binary. Builds (if sources are newer than artifacts) and runs the binary.
@@ -28,8 +30,11 @@ Builds (if sources are newer than artifacts) and runs the binary.
```bash ```bash
dcr run dcr run
dcr run --release dcr run --release
dcr run --force # force rebuild then run
``` ```
On a **workspace-only** root: if `[run].cmd` is set, that command is used after build; otherwise DCR delegates to a workspace member.
## `dcr clean` ## `dcr clean`
Removes `target/` directory (or `target/<target>/<profile>`). Removes `target/` directory (or `target/<target>/<profile>`).

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 git:https://github.com/user/repo # explicit git prefix
dcr add mylib github:user/repo # GitHub shorthand dcr add mylib github:user/repo # GitHub shorthand
dcr add mylib gitlab:user/repo # GitLab 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: Source prefixes:
@@ -22,6 +24,7 @@ Source prefixes:
- `git:` — generic git URL - `git:` — generic git URL
- `github:` — expands to `https://github.com/<user>/<repo>` - `github:` — expands to `https://github.com/<user>/<repo>`
- `gitlab:` — expands to `https://gitlab.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 - `http://` / `https://` / `git@` — full URL
Flags: Flags:
@@ -29,7 +32,12 @@ Flags:
- `--tag <name>` — git tag - `--tag <name>` — git tag
- `--rev <hash>` — git commit - `--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` ## `dcr tree`

View File

@@ -12,8 +12,12 @@ Only ASCII letters, digits, underscores `_` and hyphens `-` are allowed in the p
```bash ```bash
dcr new my-app dcr new my-app
dcr new my-app --vcs none
``` ```
Flags:
- `--vcs <git|none>` — Initialize version control system (defaults to `git` if available, or `none`).
Generates: Generates:
- `dcr.toml` with basic fields - `dcr.toml` with basic fields
- `src/main.c` with a `main` template - `src/main.c` with a `main` template
@@ -23,8 +27,11 @@ Generates:
Initializes a DCR project in the current (empty) directory. Initializes a DCR project in the current (empty) directory.
```bash ```bash
mkdir app && cd app
dcr init dcr init
dcr init --vcs none
``` ```
Flags:
- `--vcs <git|none>` — Initialize version control system (defaults to `git` if available, or `none`).
The project name is taken from the current directory name. The directory name must follow the same naming rules as `dcr new`. The project name is taken from the current directory name. The directory name must follow the same naming rules as `dcr new`.

View File

@@ -42,3 +42,18 @@ dcr fmt
Processes: `src/**/*.{c,cpp,cxx,cc,h,hpp,hxx,hh}` and `tests/**/*.{c,cpp,cxx,cc,h,hpp,hxx,hh}`. Processes: `src/**/*.{c,cpp,cxx,cc,h,hpp,hxx,hh}` and `tests/**/*.{c,cpp,cxx,cc,h,hpp,hxx,hh}`.
Uses `.clang-format` at the project root (if present), otherwise default clang-format style. Uses `.clang-format` at the project root (if present), otherwise default clang-format style.
## `dcr lint`
Runs `clang-tidy` on C/C++ source files for static analysis.
```bash
dcr lint # show diagnostics
dcr lint --fix # apply fixes automatically
dcr lint --help # detailed help
```
Processes: `src/**/*.{c,cpp,cxx,cc}` and `tests/**/*.{c,cpp,cxx,cc}`.
Without `--fix`, clang-tidy reports warnings and errors without modifying files.
With `--fix`, clang-tidy applies automatic suggestions in place.

View File

@@ -15,9 +15,9 @@ dcr setup
If `~/.dcr/config.toml` is not found, DCR returns an error. Create it manually: If `~/.dcr/config.toml` is not found, DCR returns an error. Create it manually:
```toml ```toml
[[registry]] [registry.default]
name = "default"
url = "https://index.dcr.pm" url = "https://index.dcr.pm"
priority = 1
``` ```
## `dcr --help` ## `dcr --help`

View File

@@ -8,6 +8,7 @@ sidebar_label: Contributing
```bash ```bash
git clone https://github.com/dexoron/dcr git clone https://github.com/dexoron/dcr
# or a source mirror: GitVerse / SourceCraft / SourceForge / GitLab (see README)
cd dcr cd dcr
cargo build cargo build
``` ```
@@ -42,9 +43,10 @@ cargo test --all-targets
## CI ## CI
CI runs: CI runs (see `.github/workflows/ci.yml`):
- `cargo fmt --check` - `cargo fmt --check`
- `cargo clippy --all-targets -- -D warnings` - `cargo clippy` (default + `--all-features`)
- `cargo test --all-targets` - `cargo check --all-targets --all-features`
- Build on Linux, macOS, Windows - Unit + integration tests on Linux, macOS, Windows
- Integration tests - Extra Linux job with `--features archive` (FAT images)
- Tools on runners: clang, nasm, clang-format/tidy (Linux)

View File

@@ -13,9 +13,9 @@ error: registry not found
**Solution:** Make sure `~/.dcr/config.toml` exists: **Solution:** Make sure `~/.dcr/config.toml` exists:
```toml ```toml
[[registry]] [registry.default]
name = "default"
url = "https://index.dcr.pm" url = "https://index.dcr.pm"
priority = 1
``` ```
And check `DCR_INDEX_PATH`: And check `DCR_INDEX_PATH`:
@@ -39,27 +39,6 @@ which clang
Or specify explicitly via `[toolchain]` in `dcr.toml`. Or specify explicitly via `[toolchain]` in `dcr.toml`.
## openssl-sys build failed
When building DCR from source:
```
error: failed to run custom build command for openssl-sys
```
**Solution:** Install OpenSSL development headers:
```bash
# Ubuntu/Debian
sudo apt install libssl-dev
# Fedora
sudo dnf install openssl-devel
# macOS
brew install openssl
```
## Ctrl+C during build ## Ctrl+C during build
DCR handles SIGINT: interrupts the current compilation and exits with non-zero code. DCR handles SIGINT: interrupts the current compilation and exits with non-zero code.
@@ -76,6 +55,21 @@ See [Cross-Compile Recipe](/docs/recipes/cross-to-windows).
See [Workspace Recipe](/docs/recipes/multi-package-workspace). See [Workspace Recipe](/docs/recipes/multi-package-workspace).
## Bootloader / pure NASM OS image
Use `kind = "flat-bin"` (NASM `-f bin`) and optional `[archive]` for a FAT disk image.
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 ## 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`. `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

@@ -68,3 +68,11 @@ dcr test # run tests
```bash ```bash
dcr fmt # clang-format with .clang-format dcr fmt # clang-format with .clang-format
``` ```
## Linting
```bash
dcr lint # clang-tidy checks
dcr lint --fix # apply fixes automatically
```

View File

@@ -4,25 +4,82 @@ sidebar_label: Installation
# Installation # Installation
## Linux (Dexoron Packages) ## dcrup (recommended)
Configure your package manager at https://dcr.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 ```sh
sudo pacman -Sy dcr curl -fsSL 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 ```sh
sudo apt update && sudo apt install dcr curl -fsSL https://sh.dcr-tool.ru | sh -s -- install stable
export PATH="$HOME/.dcr/bin:$PATH"
``` ```
**Fedora/RHEL** ### Windows (PowerShell)
```sh
sudo dnf install dcr ```powershell
irm https://ps1.dcr-tool.ru | iex
# or download then run:
# irm https://ps1.dcr-tool.ru -OutFile dcrup.ps1
# powershell -File .\dcrup.ps1 self-install
# $env:Path += ";$env:USERPROFILE\.dcr\bin"
dcrup install stable
``` ```
Optional cmd bootstrap (if you prefer `curl` of the cmd shim):
```bat
curl -fsSL -o dcrup.cmd https://cmd.dcr-tool.ru
```
After `self-install`, the command is **`dcrup`** (no `.sh` / `.ps1`): shims live in `~/.dcr/bin` (Unix) or `%USERPROFILE%\.dcr\bin` (Windows `dcrup.cmd`).
### 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
```
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) ## Arch Linux (AUR)
```sh ```sh
@@ -42,11 +99,12 @@ brew install dcr
sudo snap install dcrup 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) ## Nix (flake)
```sh ```sh
nix run github:dexoron/dcr nix run github:dexoron/dcr
# or install to profile:
nix profile install github:dexoron/dcr nix profile install github:dexoron/dcr
``` ```
@@ -56,51 +114,40 @@ nix profile install github:dexoron/dcr
cargo install 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 ```sh
curl -fsSL https://dcr.dexoron.su/install.sh | sh cargo install dcr --features archive
```
## BSD (script)
```sh
curl -fsSL https://dcr.dexoron.su/install_bsd.sh | sh
```
## Windows (PowerShell)
```powershell
irm https://dcr.dexoron.su/install.ps1 | iex
``` ```
## From source ## From source
```sh ```sh
git clone https://github.com/dexoron/dcr.git 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 ln -sf "$PWD/target/release/dcr" ~/.local/bin/dcr
# or manage versions with dcrup install night / --build
``` ```
## Post-install ## Post-install
Verify the installation:
```bash ```bash
dcr --version dcr --version
dcrup show # if installed via dcrup
``` ```
Man pages are available after installation: Man pages (package installs / release assets):
```bash ```bash
man dcr man dcr
man dcr-build man dcr-build
``` ```
To use the registry, create `~/.dcr/config.toml`: Registry (optional) — `~/.dcr/config.toml`:
```toml ```toml
[[registry]] [registry.default]
name = "default"
url = "https://index.dcr.pm" url = "https://index.dcr.pm"
priority = 1
``` ```

View File

@@ -6,6 +6,13 @@ sidebar_label: IDE Integration
## VS Code ## 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 ```bash
dcr gen vscode dcr gen vscode
``` ```

View File

@@ -10,36 +10,23 @@ DCR itself is [GPL-3.0-or-later](https://spdx.org/licenses/GPL-3.0-or-later.html
> **Note:** DCR is a build tool, not a library. The GPL applies only to DCR's own source code. Projects built with DCR are not subject to DCR's license — their licensing is determined solely by their own code and dependencies. > **Note:** DCR is a build tool, not a library. The GPL applies only to DCR's own source code. Projects built with DCR are not subject to DCR's license — their licensing is determined solely by their own code and dependencies.
## Vendored libraries
DCR statically links the following C libraries through its vendored Rust dependencies:
| Library | License | Bundled by |
|---|---|---|
| [libgit2](https://libgit2.org) | MIT OR Apache-2.0 | `git2` / `libgit2-sys` |
| [libssh2](https://libssh2.org) | MIT OR Apache-2.0 | `libgit2-sys` |
| [zlib](https://zlib.net) | MIT OR Apache-2.0 | `libgit2-sys` |
| [OpenSSL](https://openssl.org) | Apache-2.0 | `openssl-sys` / `openssl-src` |
| [ring](https://github.com/briansmith/ring) | Apache-2.0 AND ISC | `rustls` |
## Rust dependencies ## Rust dependencies
DCR is written in Rust. The following notable Rust crates are statically linked: DCR is written in Rust. The following notable Rust crates are statically linked:
| Crate | License | | Crate | License |
|---|---| |---|---|
| [`reqwest`](https://crates.io/crates/reqwest) | MIT OR Apache-2.0 | | [`ureq`](https://crates.io/crates/ureq) | MIT OR Apache-2.0 |
| [`rustls`](https://crates.io/crates/rustls) | Apache-2.0 OR ISC OR MIT |
| [`serde`](https://crates.io/crates/serde) | MIT OR Apache-2.0 | | [`serde`](https://crates.io/crates/serde) | MIT OR Apache-2.0 |
| [`toml`](https://crates.io/crates/toml) | MIT OR Apache-2.0 | | [`toml`](https://crates.io/crates/toml) | MIT OR Apache-2.0 |
| [`toml_edit`](https://crates.io/crates/toml_edit) | MIT OR Apache-2.0 |
| [`serde_json`](https://crates.io/crates/serde_json) | MIT OR Apache-2.0 |
| [`sha2`](https://crates.io/crates/sha2) | MIT OR Apache-2.0 | | [`sha2`](https://crates.io/crates/sha2) | MIT OR Apache-2.0 |
| [`git2`](https://crates.io/crates/git2) | MIT OR Apache-2.0 | | [`glob`](https://crates.io/crates/glob) | MIT OR Apache-2.0 |
| [`openssl`](https://crates.io/crates/openssl) | Apache-2.0 | | [`self-replace`](https://crates.io/crates/self-replace) | MIT OR Apache-2.0 |
| [`tokio`](https://crates.io/crates/tokio) | MIT |
| [`hyper`](https://crates.io/crates/hyper) | MIT |
| [`ctrlc`](https://crates.io/crates/ctrlc) | MIT OR Apache-2.0 | | [`ctrlc`](https://crates.io/crates/ctrlc) | MIT OR Apache-2.0 |
Full dependency tree (143 packages) is available in [`Cargo.lock`](https://github.com/dexoron/dcr/blob/main/Cargo.lock). The overwhelming majority are dual-licensed under MIT OR Apache-2.0. Full dependency tree is available in [`Cargo.lock`](https://github.com/dexoron/dcr/blob/main/Cargo.lock). The overwhelming majority are dual-licensed under MIT OR Apache-2.0.
## Additional credits ## Additional credits

View File

@@ -34,6 +34,7 @@ type = "none"
inherit = true inherit = true
language = "c" language = "c"
standard = "c11" standard = "c11"
workspace_only = true
[workspace.lib-core] [workspace.lib-core]
path = "lib-core" path = "lib-core"

View File

@@ -0,0 +1,89 @@
---
sidebar_label: OS-dev (flat-bin + archive)
---
# Pure ASM / boot image (flat-bin + archive)
Minimal OS-dev style pipeline: assemble raw binaries with NASM, then pack a FAT floppy/image with an optional boot sector.
## Project layout
```
myos/
dcr.toml
src/
boot.asm # boot sector (512 bytes)
kernel.asm # payload
```
For multiple flat artifacts, use a **workspace** (one member per binary) or separate packages; a single `flat-bin` package produces one binary per source stem.
## Boot package (`flat-bin`)
```toml
[package]
name = "boot"
version = "0.1.0"
[build]
language = "asm"
compiler = "nasm"
kind = "flat-bin"
extension = "bin"
roots = ["src/boot.asm"]
```
```bash
dcr build
# → target/<…>/debug/boot.bin (NASM -f bin, no link)
```
## Disk image after build
```toml
[archive]
output = "target/{profile}/disk.img"
format = "fat12"
size = "1440K"
label = "MYOS"
bootsector = "target/{profile}/boot.bin"
[[archive.layout]]
from = "target/{profile}/kernel.bin"
to = "KERNEL.BIN"
```
- `format`: `fat12`, `fat16`, or `fat32`
- `size`: bytes or `K`/`KB`/`M`/`MB`/`G`/`GB` (default ~1.44 MiB)
- `bootsector`: written only when `offset` is omitted or `0`
- `from` may be a glob; `{profile}` is substituted in paths
## C kernel → flat binary
```toml
[build]
language = "c"
compiler = "clang"
kind = "flat-bin"
freestanding = true
filename = "kernel"
extension = "bin"
ldflags = ["-T", "linker.ld"]
```
Pipeline: objects → temporary linked ELF → `objcopy -O binary` → `kernel.bin`. Requires `objcopy` / `llvm-objcopy` in PATH.
## Other assemblers
| Tool | Notes |
|------|--------|
| **FASM** | Write `format binary` in the source; DCR writes `<stem>.bin` directly |
| **GAS** | Assemble `.s` → obj → `objcopy -O binary` |
| **LLC** | `language = "llvm_ir"` → obj → objcopy |
| **MASM** | COFF obj → objcopy (needs binutils/LLVM objcopy) |
## Notes
- Single-file `roots` are supported: `roots = ["src/boot.asm"]`.
- `--force` re-runs `build.steps` / `build.post_steps` as well as recompilation.
- Prefer `kind = "elf"` if you need a relocatable ELF kernel without stripping to raw binary.

View File

@@ -32,11 +32,37 @@ Set `inherit = false` to disable array inheritance (only profile's own arrays ar
## Built-in profiles ## Built-in profiles
| Profile | Default cflags | The default flags for each profile are composed from three config fields:
| Field | debug default | release default |
|-------|--------------|----------------|
| `opt_level` | `"0"` | `"3"` |
| `debug` | `true` | `false` |
| `warnings` | `["all", "extra"]` | `[]` |
Which produce the equivalent compiler flags:
| Profile | Effective flags |
|---------|---------------| |---------|---------------|
| `debug` | `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG` | | `debug` | `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG` |
| `release` | `-O3 -DNDEBUG` | | `release` | `-O3 -DNDEBUG` |
Additional build options can be toggled per-profile:
```toml
[build.release]
opt_level = "z"
lto = true
strip = true
panic = "abort"
codegen-units = "1"
[build.debug]
opt_level = "1"
debug = false
warnings = ["all", "error"]
```
## Target-specific profiles ## Target-specific profiles
```toml ```toml

View File

@@ -6,30 +6,60 @@ sidebar_label: Build system
## Compiler Backends ## Compiler Backends
DCR supports 4 compilation backends. Selection is automatic based on file extension. DCR supports 7 compilation backends. Selection is automatic based on file extension and `build.compiler` value.
| Backend | Files | When used | | Backend | Files | When used |
|---------|-------|-----------| |---------|-------|-----------|
| `unix_cc` | `.c`, `.cpp`, `.cxx`, `.cc`, `.S` | gcc/clang on Linux/macOS/BSD | | `unix_cc` | `.c`, `.cpp`, `.cxx`, `.cc`, `.S` | gcc/clang on Linux/macOS/BSD |
| `msvc` | `.c`, `.cpp`, `.cxx`, `.cc` | Windows (cl, clang-cl) | | `msvc` | `.c`, `.cpp`, `.cxx`, `.cc` | Windows (cl, clang-cl) |
| `gas` | `.s` | ARM/ARM64 assembler (no preprocessor) | | `gas` | `.s` | ARM/ARM64 assembler (no preprocessor) |
| `nasm` | `.asm` | x86/x86_64 assembler | | `nasm` | `.asm`, `.s` | x86/x86_64 NASM assembler |
| `masm` | `.asm` | MASM (ml/ml64) on Windows |
| `fasm` | `.asm`, `.fasm` | Flat Assembler |
| `llvm_ir` | `.ll` | LLVM IR via `llc -filetype=obj` |
## Qt Support
DCR provides native Qt support for automatic meta-object handling (MOC, UIC, RCC).
Enable it by setting `build.qt = true` in `dcr.toml`. DCR will automatically detect Qt-related files (`.ui`, `.qrc`, `.h` with `Q_OBJECT`) and process them.
```toml
[build]
qt = true
```
*Note: Requires `qt6` modules (Core, Widgets, Gui, Svg) installed via `pkg-config`.*
Advanced customization is still possible via `build.steps` if special handling is needed:
```toml
[build.steps]
moc = "moc {in} -o {out}"
```
### Unix CC ### Unix CC
- Compiler resolved via `resolve_compiler()`: `DCR_COMPILER` > `DCR_CC` > `[toolchain]` > `build.compiler` > `PATH` - Compiler resolved via `resolve_compiler()`: `DCR_COMPILER` > `DCR_CC` > `[toolchain]` > `build.compiler` > `PATH`
- Supports `.d` files for header dependency tracking - Supports `.d` files for header dependency tracking
- Flags: `-std=`, `-MMD -MF`, `-c -o`, `-I`, `-L`, `-l` - Flags: `-std=`, `-MMD -MF`, `-c -o`, `-I`, `-L`, `-l`
- Conditional flags based on config:
- `freestanding` or bare-metal target: `-ffreestanding` (compile), `-nostdlib -static` (link)
- `lto`: `-flto` (compile + link)
- `panic = "abort"`: `-fno-exceptions` (C++ only), `-fno-unwind-tables`, `-fno-asynchronous-unwind-tables`
### MSVC ### MSVC
- Supports cl.exe and clang-cl.exe - Supports cl.exe and clang-cl.exe
- Flags: `/std:`, `/Fo:`, `/Fe:`, `/I`, `/link` - Flags: `/std:`, `/Fo:`, `/Fe:`, `/I`, `/link`
### GAS / NASM ### GAS / NASM / MASM / FASM / LLVM-IR
- GAS: `-I`, `-c -o`, `--defsym` - GAS: `-I`, `-c -o`, `--defsym`
- NASM: `-I`, `-o`, `-D`, `-f` (format: win64/elf64/macho64/macho32/elf32) - NASM: `-I`, `-o`, `-D`, `-f` (format: win64/elf64/macho64/macho32/elf32; **`bin` when `kind = "flat-bin"`**)
- MASM: `/nologo /c /Fo<obj> <source>`
- FASM: `<source> <output>` (output path is the object file)
- LLVM IR: `-filetype=obj <source> -o <output>`
## Incremental Builds ## Incremental Builds
@@ -44,7 +74,7 @@ Three levels of incrementality:
- `thread::scope` for thread pool - `thread::scope` for thread pool
- Atomic task queue (`AtomicU64`) - Atomic task queue (`AtomicU64`)
- Mutex on stdout (`OUTPUT_MUTEX`) - Mutex on stdout (`OUTPUT_MUTEX`)
- Thread count = `available_parallelism()` - Thread count = `available_parallelism()`, capped by `build.codegen-units` if set
## Build Kinds ## Build Kinds
@@ -57,6 +87,11 @@ Three levels of incrementality:
| `elf` | ELF without stdlib | `target/<triple>/<profile>/<name>` | | `elf` | ELF without stdlib | `target/<triple>/<profile>/<name>` |
| `none` | Compile only, no link | — | | `none` | Compile only, no link | — |
| `custom` | Full filename+extension control | `target/<triple>/<profile>/<filename>.<extension>` | | `custom` | Full filename+extension control | `target/<triple>/<profile>/<filename>.<extension>` |
| `flat-bin` | Raw binary | ASM: `<stem>.bin` (NASM `-f bin` / FASM / GAS·MASM·LLC via objcopy); C/C++: `<name>.bin` (link + objcopy) |
## Disk images (`[archive]`)
After a successful build, if `[archive]` is present in `dcr.toml`, DCR formats a FAT volume and copies files from `layout` into the image. See [dcr.toml → archive](/docs/reference/dcr-toml#archive).
## Build Steps ## Build Steps

View File

@@ -37,9 +37,19 @@ target = "aarch64-unknown-linux-gnu"
# Auto: cflags += ["--target=aarch64-unknown-linux-gnu"] # Auto: cflags += ["--target=aarch64-unknown-linux-gnu"]
``` ```
## Bare-Metal ## Bare-Metal / Freestanding
For targets containing `none`, `-elf`, `eabi`, or `baremetal`, DCR default flags are disabled (no system include paths, no `-l` libc). For targets containing `none`, `-elf`, `eabi`, or `baremetal`, DCR automatically:
1. **Disables default flags** — no system include paths, no `-l` libc
2. **Injects `-ffreestanding`** at compile time and `-nostdlib -static` at link time
You can also enable freestanding mode explicitly:
```toml
[build]
freestanding = true
```
```bash ```bash
dcr build --target aarch64-none-elf dcr build --target aarch64-none-elf
@@ -47,14 +57,8 @@ dcr build --target aarch64-none-elf
## Target Directory ## Target Directory
On Linux with explicit target: By default, the compilation output directories are structured as follows:
``` - **Linux and BSD**: Always output to `target/<triple>/<profile>/` (using host triple if no target is specified).
target/<triple>/<profile>/ - **macOS and Windows (without target)**: Output to `target/<profile>/`.
``` - **macOS and Windows (with explicit target)**: Output to `target/<triple>/<profile>/`.
On macOS/Windows or without target:
```
target/<profile>/
```

View File

@@ -33,6 +33,9 @@ Main project configuration file. Located at the project root.
[run] [run]
# run settings # run settings
[archive]
# optional: pack FAT disk image after build
``` ```
## [package] ## [package]
@@ -40,8 +43,9 @@ Main project configuration file. Located at the project root.
| Field | Required | Description | | Field | Required | Description |
|-------|----------|-------------| |-------|----------|-------------|
| `name` | yes | Project name | | `name` | yes | Project name |
| `version` | yes | Semantic version | | `version` | yes | Semantic version of the **project** |
| `type` | yes | `app`, `lib`, `none` | | `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 | | `license` | no | SPDX license identifier |
| `author` | no | Author | | `author` | no | Author |
@@ -49,40 +53,99 @@ Main project configuration file. Located at the project root.
[package] [package]
name = "my-app" name = "my-app"
version = "0.1.0" version = "0.1.0"
dcr-version = "0.8.4"
type = "app" type = "app"
license = "MIT" license = "MIT"
author = "John Doe" 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] ## [build]
...
- `build.qt` — (bool) Enable automatic Qt meta-object handling (MOC, UIC, RCC). Requires `qt6` modules installed via `pkg-config`.
| Field | Default | Description | | Field | Default | Description |
|-------|---------|-------------| |-------|---------|-------------|
| `language` | `"c"` | `"c"`, `"c++"`, `"cpp"`, `"cxx"`, `"asm"` | | `language` | `"c"` | `"c"`, `"c++"`, `"cpp"`, `"cxx"`, `"asm"`, `"llvm_ir"`, `"llvm-ir"`, `"ll"` (optional, defaults to `"c"`) |
| `standard` | `"c11"` | C standard (`c11`, `c17`, `c23`) | | `standard` | `"c11"` | C standard (`c11`, `c17`, `c23`) |
| `cxx_standard` | `"c++20"` | C++ standard (`c++17`, `c++20`, `c++23`) | | `cxx_standard` | — | C++ standard (`c++17`, `c++20`, `c++23`) |
| `compiler` | `"clang"` | Preferred compiler | | `compiler` | `"clang"` | Preferred compiler (optional, defaults to `"clang"`) |
| `kind` | `"bin"` | `bin`, `staticlib`, `sharedlib`, `efi`, `elf`, `none`, `custom` | | `kind` | `"bin"` | `bin`, `staticlib`, `sharedlib`, `efi`, `elf`, `none`, `custom`, `flat-bin` |
| `target` | host | Target triple for cross-compilation | | `target` | host | Target triple for cross-compilation |
| `platform` | `"native"` | `native`, `efi`, `freestanding` | | `platform` | `"native"` | `native`, `efi` |
| `cflags` | `[]` | Additional C/C++/ASM flags | | `cflags` | `[]` | Additional C/C++/ASM flags |
| `ldflags` | `[]` | Additional linker flags | | `ldflags` | `[]` | Additional linker flags |
| `filename` | `""` | Custom output file name | | `filename` | `""` | Custom output file name |
| `extension` | `""` | Custom file extension | | `extension` | `""` | Custom file extension (for `flat-bin`, default is `bin`) |
| `roots` | `["src"]` | Source root directories | | `roots` | `["src"]` | Source roots: directories and/or individual source/header files |
| `exclude` | `[]` | Exclude patterns | | `exclude` | `[]` | Exclude patterns |
| `include` | `[]` | Additional include directories | | `include` | `[]` | Additional include directories |
| `src_disable` | `false` | Disable auto source discovery | | `src_disable` | `false` | Disable auto source discovery |
| `inherit` | `false` | Inherit build from workspace root | | `inherit` | `false` | Inherit build from workspace root |
| `clean` | `[]` | Glob patterns for custom clean paths | | `clean` | `[]` | Glob patterns for custom clean paths |
| `out_dir` | `""` | Custom output directory | | `out_dir` | `""` | Custom output directory |
| `workspace_only` | `false` | Workspace-only, not built standalone | | `workspace_only` | `false` | Workspace-only, not built standalone (no `language`/`compiler` required) |
| `freestanding` | `false` | Compile in freestanding mode (`-ffreestanding` + `-nostdlib -static`) |
| `opt_level` | — | Optimization level: `0`-`3`, `"s"`, `"z"` (derived from profile if omitted) |
| `debug` | profile-based | Emit debug symbols (`-g`): `true` in debug, `false` in release |
| `lto` | `false` | Link-time optimization (`-flto` for both compiler and linker) |
| `strip` | `false` | Strip symbols from output (`-s` in ldflags) |
| `warnings` | `[]` | Warning flags (e.g. `"all"`, `"extra"`, `"pedantic"`); engine adds `-Wall -Wextra` in debug if empty |
| `panic` | `""` | Panic strategy: `"abort"` disables exceptions and unwind tables |
| `codegen-units` | `""` | Max parallel compilation jobs (`"0"` = auto) |
| `qt` | `false` | Enable automatic Qt meta-object handling (MOC, UIC, RCC) |
Settings from raw config (not in typed struct): 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 - `ldscript` — linker script path
- `build.steps` / `build.post_steps` — codegen steps - `build.steps` / `build.post_steps` — codegen steps
## Per-language overrides: `[build.c]`, `[build.cxx]`, `[build.asm]`, `[build.llvm_ir]`
Each language can have its own table that overrides the flat `[build]` settings:
```toml
[build]
compiler = "clang"
standard = "c11"
[build.c]
standard = "c23"
compiler = "gcc"
[build.cxx]
standard = "c++23"
compiler = "g++"
[build.asm]
compiler = "nasm"
flags = ["-felf64"]
[build.llvm_ir]
compiler = "llc"
```
The flat `[build]` acts as fallback; per-language tables take precedence for their language.
Example: Example:
```toml ```toml
@@ -93,6 +156,11 @@ cxx_standard = "c++23"
compiler = "clang" compiler = "clang"
kind = "sharedlib" kind = "sharedlib"
cflags = ["-Wall", "-Wextra"] cflags = ["-Wall", "-Wextra"]
opt_level = "z"
lto = true
strip = true
panic = "abort"
codegen-units = "2"
``` ```
## [toolchain] ## [toolchain]
@@ -130,3 +198,84 @@ Default `cmd` = `./target/{profile}/{name}` (macOS/Windows) or `./target/<triple
## [workspace] ## [workspace]
See [workspaces](/docs/reference/workspaces). See [workspaces](/docs/reference/workspaces).
## [archive]
Optional post-build step: format a FAT volume and copy built artifacts into a disk image. Runs after a successful package build (and after workspace member builds that define `[archive]`).
Requires DCR built with the `archive` Cargo feature (`cargo build --features archive`). Release binaries include this feature.
| Field | Required | Description |
|-------|----------|-------------|
| `output` | yes | Image path relative to project root (`{profile}` allowed) |
| `format` | yes | `fat12`, `fat16`, or `fat32` |
| `size` | no | Image size: bytes or `K`/`KB`/`M`/`MB`/`G`/`GB` (default `1474560` ≈ 1.44 MiB) |
| `offset` | no | Byte offset of the FAT volume inside the image (default `0`) |
| `label` | no | Volume label (max 11 chars, default `VOLUME`) |
| `bootsector` | no | Path to a 512-byte boot sector written at offset 0 when `offset` is 0 (`{profile}` allowed) |
| `layout` | no | List of `{ from, to }` entries (files or globs → path inside the volume) |
```toml
[archive]
output = "target/{profile}/disk.img"
format = "fat12"
size = "1440K"
label = "MYOS"
bootsector = "target/{profile}/boot.bin"
[[archive.layout]]
from = "target/{profile}/kernel.bin"
to = "KERNEL.BIN"
[[archive.layout]]
from = "assets/*"
to = "/"
```
Typical pairing with `kind = "flat-bin"` (NASM `-f bin`) for bootloaders and pure-ASM OS images.
## `flat-bin` (kind)
Produces a raw binary (default extension `bin`) for boot sectors, kernels, and freestanding payloads.
### Assemblers
| Tool | Language / compiler | How flat-bin is produced |
|------|---------------------|--------------------------|
| NASM | `language = "asm"`, `compiler = "nasm"` | `-f bin` → `<stem>.bin` |
| FASM | `compiler = "fasm"` | direct write (use `format binary` in source) |
| GAS | `compiler = "as"` / `"gas"` | assemble → `objcopy -O binary` |
| MASM | `compiler = "ml"` / `"ml64"` | assemble → `objcopy -O binary` |
| LLC | `language = "llvm_ir"`, `compiler = "llc"` | `-filetype=obj` → `objcopy -O binary` |
```toml
[build]
language = "asm"
compiler = "nasm"
kind = "flat-bin"
extension = "bin"
roots = ["src/boot.asm"]
```
### C / C++
Compile all sources, link with `-nostdlib -static` (plus your `ldflags` / `ldscript`), then convert the intermediate ELF/PE with `objcopy -O binary` to `<filename|name>.bin`.
```toml
[build]
language = "c"
compiler = "clang"
kind = "flat-bin"
freestanding = true
filename = "kernel"
extension = "bin"
ldscript = "linker.ld"
ldflags = ["-T", "linker.ld"]
```
Notes:
- Multi-file **ASM** packages emit one `<stem>.bin` per source; **C/C++** emit a single project binary.
- `objcopy` tools tried in order: `llvm-objcopy`, `objcopy`, `gobjcopy`.
- Incompatible with `build.qt = true`.
- `dcr run` rejects `flat-bin` (not a host executable).

View File

@@ -6,7 +6,7 @@ sidebar_label: Dependencies
## Formats ## Formats
The `[dependencies]` section supports three formats: The `[dependencies]` section supports registry, path, Git, and `pkg-config` dependencies:
### String (registry) ### String (registry)
@@ -23,10 +23,10 @@ The version string is used as-is for registry lookup.
```toml ```toml
[dependencies] [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) ### Table (path)
@@ -35,33 +35,73 @@ Fields: `git`, `branch`, `tag`, `rev`.
mylib = { path = "../mylib" } 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 ## Registry
DCR uses a package registry for dependency lookup by name. DCR uses a package registry for dependency lookup by name.
```toml ```toml
# ~/.dcr/config.toml # ~/.dcr/config.toml
[[registry]] [registry.default]
name = "default" url = "https://index.dcr.dexoron.tech"
url = "https://index.dcr.pm" priority = 1
``` ```
Registry priority: order in `config.toml`. The `DCR_INDEX_PATH` variable overrides the path to `index.json`. Registry priority: order in `config.toml`. The `DCR_INDEX_PATH` variable overrides the path to `index.json`.
## Git dependencies ## Git dependencies
DCR clones git repositories via `git2`: 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 - `branch` — switch to a branch
- `tag` — switch to a tag - `tag` — switch to a tag
- `rev` — switch to a specific commit - `rev` — switch to a specific commit
- `features` — feature flags (parsed, but does not affect build) - `features` — feature flags (parsed, but does not affect build)
Cache path: `~/.dcr/git/<hash>/`.
## Path dependencies ## 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 ```toml
[dependencies] [dependencies]
@@ -69,16 +109,51 @@ mylib = { path = "/abs/path/to/lib" }
mylib = { path = "../relative/path" } mylib = { path = "../relative/path" }
``` ```
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 ## dcr.lock
Dependency lock file. Contains package names and sources. 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 ## Resolution process
1. Load all dependencies (registry → git → path) 1. Resolve dependency sources (registry → git → path)
2. For path deps: recursively read their `dcr.toml` 2. Fetch git dependencies into `~/.dcr/git/` and check out the requested ref
3. For registry deps: search `index.json` 3. Build DCR package dependencies recursively
4. For git deps: clone to cache 4. Collect `include_dirs`, `lib_dirs`, `libs` for the compiler
5. 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`. 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

@@ -11,17 +11,18 @@ DCR normalizes target triples. Short platform names are expanded to full triples
### Linux ### Linux
``` ```
target/<arch>-unknown-linux-gnu/<profile>/ target/<arch>-unknown-linux-<env>/<profile>/
``` ```
Architectures: `x86_64`, `aarch64` (host-detected). Architectures: `x86_64`, `aarch64`, `i686`, `armv7`, `riscv64` (host-detected).
Environments: `gnu` (default), `musl`.
Artifact type: ELF. Extensions: `.so` (sharedlib), `.a` (staticlib). Artifact type: ELF. Extensions: `.so` (sharedlib), `.a` (staticlib).
### macOS ### macOS
``` ```
target/<profile>/ target/<profile>/ (default) or target/<triple>/<profile>/ (with target)
``` ```
Architectures: `x86_64`, `aarch64` (host-detected). Architectures: `x86_64`, `aarch64` (host-detected).
@@ -31,10 +32,11 @@ Extensions: `.dylib` (sharedlib), `.a` (staticlib).
### Windows ### Windows
``` ```
target/<profile>/ target/<profile>/ (default) or target/<triple>/<profile>/ (with target)
``` ```
Architectures: `x86_64`, `aarch64`. Architectures: `x86_64`, `aarch64`.
Environments: `msvc` (default), `gnu` (MinGW).
Extensions: `.exe` (bin), `.lib` (staticlib), `.dll` (sharedlib), `.efi` (UEFI). Extensions: `.exe` (bin), `.lib` (staticlib), `.dll` (sharedlib), `.efi` (UEFI).

View File

@@ -43,7 +43,9 @@ dcr build # build all packages in dependency order
dcr build --workspace app # build only app (dependencies built automatically) dcr build --workspace app # build only app (dependencies built automatically)
``` ```
When building a workspace, member outputs (libraries) are automatically added to include/lib paths of dependent packages. When building a workspace, DCR automatically injects include and library paths of dependent workspace members:
- **Include Paths**: Automatically resolves and injects header directories of dependencies, including the member's `src/` directory, local `include/` directory, and the packaged `target/include` directory.
- **Library Paths**: Injects compiled library search paths (`target/lib` as well as target-specific build folders) to allow automatic linking with member libraries.
## Clean ## Clean
@@ -65,3 +67,21 @@ standard = "c17"
# member inherits language and standard # member inherits language and standard
``` ```
## Workspace-only root (no build of its own)
A workspace root can set `workspace_only = true` — it won't be built itself, and doesn't need `language` or `compiler`:
```toml
[package]
name = "my-workspace"
version = "0.1.0"
[build]
workspace_only = true
kind = "bin"
[workspace]
lib-core = { path = "lib-core" }
app = { path = "app", deps = ["lib-core"] }
```

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 .B path:./path/to/lib
Local path dependency. Local path dependency.
.TP .TP
.B pkg-config:name
System pkg-config dependency (also accepts \fBpkg:\fR or comma-separated packages).
.TP
.B <version> .B <version>
Version from registry. Version from registry.
.SH OPTIONS .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 .SH NAME
dcr-build \- Compile a DCR project dcr-build \- Compile a DCR project
.SH SYNOPSIS .SH SYNOPSIS
@@ -9,8 +9,15 @@ dcr-build \- Compile a DCR project
[\fB\-\-clean\fR] [\fB\-\-clean\fR]
[\fB\-\-verbose\fR] [\fB\-\-verbose\fR]
[\fB\-\-workspace\fR \fI<name>\fR] [\fB\-\-workspace\fR \fI<name>\fR]
[\fB\-\-print\-artifact\-path\fR]
.SH DESCRIPTION .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 .SH OPTIONS
.TP .TP
.B \-\-debug .B \-\-debug
@@ -33,5 +40,9 @@ Print detailed build output including compiler commands.
.TP .TP
.B \-\-workspace \fI<name>\fR .B \-\-workspace \fI<name>\fR
Build a specific workspace member. 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 .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 .SH NAME
dcr-gen \- Generate IDE and tooling integration files dcr-gen \- Generate IDE and tooling integration files
.SH SYNOPSIS .SH SYNOPSIS
.B dcr gen \fI<subcommand>\fR .B dcr gen \fI<subcommand>\fR
[\fB\-\-debug\fR | \fB\-\-release\fR]
[\fB\-\-quiet\fR | \fB\-q\fR]
.SH DESCRIPTION .SH DESCRIPTION
Generates IDE and tooling integration files for DCR projects. 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 .SH SUBCOMMANDS
.TP .TP
.B project-info .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 .TP
.B compile-commands .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 .TP
.B vscode .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 .TP
.B clion .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 .SH SEE ALSO
.BR dcr (1), .BR dcr (1),
.BR dcr-build (1) .BR dcr-build (1)

View File

@@ -8,9 +8,13 @@ dcr-run \- Build and run a DCR project
[\fB\-\-force\fR] [\fB\-\-force\fR]
[\fB\-\-clean\fR] [\fB\-\-clean\fR]
[\fB\-\-verbose\fR] [\fB\-\-verbose\fR]
[\fB\-\-\fR \fI<args>...\fR]
.SH DESCRIPTION .SH DESCRIPTION
Builds and runs the project. Only available for projects with Builds and runs the project. Only available for projects with
\fBkind = "bin"\fR. \fBkind = "bin"\fR.
Arguments after a bare
.B \-\-
are forwarded to the built binary (cargo-style).
.SH OPTIONS .SH OPTIONS
.TP .TP
.B \-\-debug .B \-\-debug
@@ -30,6 +34,14 @@ Clean before building.
.TP .TP
.B \-\-verbose .B \-\-verbose
Print detailed build output. 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 .SH SEE ALSO
.BR dcr (1), .BR dcr (1),
.BR dcr-build (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-08-31" "dcr 0.9.1" "DCR Manual"
.SH NAME .SH NAME
dcr \- Cargo\-like build tool and package manager for C/C++ projects dcr \- Cargo\-like build tool and package manager for C/C++ projects
.SH SYNOPSIS .SH SYNOPSIS
@@ -38,11 +38,14 @@ Display the dependency tree of the current project.
.B fmt .B fmt
Format all C/C++ source files using clang-format. Format all C/C++ source files using clang-format.
.TP .TP
.B lint [\fI\-\-fix\fR]
Run clang-tidy static analysis on C/C++ sources.
.TP
.B gen \fI<subcommand>\fR .B gen \fI<subcommand>\fR
Generate IDE integration files (vscode, clion, compile-commands, project-info). Generate IDE integration files (vscode, clion, compile-commands, project-info).
.TP .TP
.B setup .B setup
Set up DCR registries for dependency resolution. Show configured package registries.
.SH GLOBAL FLAGS .SH GLOBAL FLAGS
.TP .TP
.B \-\-help .B \-\-help
@@ -117,6 +120,9 @@ Generic git repository.
.B path:./path/to/lib .B path:./path/to/lib
Local path dependency. Local path dependency.
.TP .TP
.B pkg-config:name
System pkg-config dependency (also accepts \fBpkg:name1,name2\fR).
.TP
.B \fI<version>\fR .B \fI<version>\fR
Version from registry (e.g. \fB1.2.3\fR). Version from registry (e.g. \fB1.2.3\fR).
.SH GEN SUBCOMMANDS .SH GEN SUBCOMMANDS
@@ -125,7 +131,7 @@ Version from registry (e.g. \fB1.2.3\fR).
Print project metadata as JSON to stdout. Print project metadata as JSON to stdout.
.TP .TP
.B compile-commands .B compile-commands
Generate \fBcompile_commands.json\fR for IDE tooling. Generate \fB.dcr/compile_commands.json\fR for IDE tooling.
.TP .TP
.B vscode .B vscode
Generate \fB.vscode/\fR integration files (tasks, launch configurations). Generate \fB.vscode/\fR integration files (tasks, launch configurations).
@@ -151,6 +157,11 @@ Project name (required).
.B package.version .B package.version
Project version (default: \fB0.1.0\fR). Project version (default: \fB0.1.0\fR).
.TP .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 .B build.language
Programming language: \fBc\fR or \fBc++\fR (default: \fBc\fR). Programming language: \fBc\fR or \fBc++\fR (default: \fBc\fR).
.TP .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,7 +15,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::config::Config; use crate::core::build_config::Config;
use crate::core::deps::register; use crate::core::deps::register;
use crate::utils::fs::find_project_root; use crate::utils::fs::find_project_root;
use crate::utils::log::error; use crate::utils::log::error;
@@ -23,16 +23,40 @@ use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use toml::Value; use toml::Value;
use toml::map::Map; use toml::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 { pub struct AddArgs {
/// Dependency name as it will appear under `[dependencies]` in `dcr.toml`.
pub name: String, pub name: String,
/// Local filesystem path when the source uses the `path:` prefix.
pub path: Option<String>, pub path: Option<String>,
/// Git repository URL when the source is a git/GitHub/GitLab URL or prefix.
pub git: Option<String>, pub git: Option<String>,
/// Optional git branch to pin the dependency to.
pub branch: Option<String>, pub branch: Option<String>,
/// Optional git tag to pin the dependency to.
pub tag: Option<String>, pub tag: Option<String>,
/// Optional git commit hash (revision) to pin the dependency to.
pub rev: Option<String>, pub rev: Option<String>,
/// Version string resolved from the package registry when no explicit source is given.
pub version_from_registry: Option<String>, 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 { pub fn add(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN); printc("USAGE:", BOLD_GREEN);
@@ -45,11 +69,11 @@ pub fn add(args: &[String]) -> i32 {
println!(" Adds a dependency to the project."); println!(" Adds a dependency to the project.");
println!(); println!();
printc("SOURCES:", BOLD_GREEN); printc("SOURCES:", BOLD_GREEN);
println!(" user/repo GitHub shorthand");
println!(" github:user/repo GitHub repository"); println!(" github:user/repo GitHub repository");
println!(" gitlab:user/repo GitLab repository"); println!(" gitlab:user/repo GitLab repository");
println!(" git:host.com/user/repo Generic git repository"); println!(" git:host.com/user/repo Generic git repository");
println!(" path:./path/to/lib Local path dependency"); println!(" path:./path/to/lib Local path dependency");
println!(" pkg-config:fmt System pkg-config dependency");
println!(" <version> Version from registry"); println!(" <version> Version from registry");
println!(); println!();
printc("OPTIONS:", BOLD_GREEN); printc("OPTIONS:", BOLD_GREEN);
@@ -92,6 +116,7 @@ pub fn add(args: &[String]) -> i32 {
} }
}; };
// Build the TOML value written under dependencies.<name>
let dep_value = if let Some(git_url) = add_args.git { 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() { if add_args.branch.is_none() && add_args.tag.is_none() && add_args.rev.is_none() {
let mut table = Map::new(); let mut table = Map::new();
@@ -111,6 +136,17 @@ pub fn add(args: &[String]) -> i32 {
} }
Value::Table(table) 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 { } else if let Some(path) = add_args.path {
let mut table = Map::new(); let mut table = Map::new();
table.insert("path".to_string(), Value::String(path)); table.insert("path".to_string(), Value::String(path));
@@ -118,7 +154,7 @@ pub fn add(args: &[String]) -> i32 {
} else if let Some(version) = add_args.version_from_registry { } else if let Some(version) = add_args.version_from_registry {
Value::String(version) Value::String(version)
} else { } else {
error("Dependency source (path or git) must be provided"); error("Dependency source (path, git, or pkg-config) must be provided");
return 1; return 1;
}; };
@@ -136,15 +172,20 @@ pub fn add(args: &[String]) -> i32 {
0 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> { fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
if args.is_empty() { if args.is_empty() {
error("Usage: dcr add <name> <source> [--branch <branch> | --tag <tag> | --rev <rev>]"); error("Usage: dcr add <name> <source> [--branch <branch> | --tag <tag> | --rev <rev>]");
error("Sources:"); error("Sources:");
error(" user/repo -> github.com/user/repo");
error(" github:user/repo -> github.com/user/repo"); error(" github:user/repo -> github.com/user/repo");
error(" gitlab:user/repo -> gitlab.com/user/repo"); error(" gitlab:user/repo -> gitlab.com/user/repo");
error(" git:host.com/user/repo -> host.com/user/repo"); error(" git:host.com/user/repo -> host.com/user/repo");
error(" path:./path/to/lib -> local path"); error(" path:./path/to/lib -> local path");
error(" pkg-config:fmt -> system pkg-config package");
return Err(1); return Err(1);
} }
@@ -170,7 +211,7 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
} }
source_spec = s.clone(); source_spec = s.clone();
} else { } else {
// No source provided — try registry auto-lookup // No source provided — resolve version from the package registry
match register::resolve_package_from_registry(&name) { match register::resolve_package_from_registry(&name) {
Ok(info) => { Ok(info) => {
let version = info let version = info
@@ -195,6 +236,7 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
tag: None, tag: None,
rev: None, rev: None,
version_from_registry: Some(version.to_string()), version_from_registry: Some(version.to_string()),
pkg_config: None,
}); });
} }
Err(e) => { Err(e) => {
@@ -236,8 +278,25 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
let mut path = None; let mut path = None;
let mut git = 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()); path = Some(p.to_string());
} else if let Some(g) = source_spec.strip_prefix("github:") { } else if let Some(g) = source_spec.strip_prefix("github:") {
git = Some(format!("https://github.com/{}", g)); git = Some(format!("https://github.com/{}", g));
@@ -245,10 +304,10 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
git = Some(format!("https://gitlab.com/{}", g)); git = Some(format!("https://gitlab.com/{}", g));
} else if let Some(g) = source_spec.strip_prefix("git:") { } else if let Some(g) = source_spec.strip_prefix("git:") {
if g.contains('/') && !g.contains('.') { 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)); git = Some(format!("https://github.com/{}", g));
} else { } 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@") { let url = if g.starts_with("http") || g.starts_with("git@") {
g.to_string() g.to_string()
} else { } else {
@@ -261,8 +320,15 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|| source_spec.starts_with("git@") || source_spec.starts_with("git@")
{ {
git = Some(source_spec); 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 { } 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); return Err(1);
} }
@@ -274,5 +340,6 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
tag, tag,
rev, rev,
version_from_registry: None, version_from_registry: None,
pkg_config,
}) })
} }

File diff suppressed because it is too large Load Diff

View File

@@ -15,10 +15,9 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::config::flags; use crate::core::build_config::Config;
use crate::core::config::Config;
use crate::core::workspace::parse_workspace; use crate::core::workspace::parse_workspace;
use crate::utils::build::{default_target_triple, parse_version_info}; use crate::utils::build::{default_profile_flags, default_target_triple, parse_version_info};
use crate::utils::fs::{check_dir, find_project_root, with_dir}; use crate::utils::fs::{check_dir, find_project_root, with_dir};
use crate::utils::log::{error, warn}; use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc}; use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
@@ -26,6 +25,15 @@ use glob::glob;
use std::fs; use std::fs;
use std::path::Path; use std::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 { pub fn clean(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN); printc("USAGE:", BOLD_GREEN);
@@ -71,6 +79,7 @@ pub fn clean(args: &[String]) -> i32 {
} }
}; };
// Run cleanup relative to the project root so paths resolve consistently.
match with_dir(&root, || clean_from_root(&root, &flags)) { match with_dir(&root, || clean_from_root(&root, &flags)) {
Ok(()) => 0, Ok(()) => 0,
Err(msg) => { Err(msg) => {
@@ -80,12 +89,20 @@ pub fn clean(args: &[String]) -> i32 {
} }
} }
/// Parsed flags controlling which artifacts `clean` should remove.
struct CleanFlags { struct CleanFlags {
/// Optional build profile name (e.g. `debug`, `release`).
profile: Option<String>, profile: Option<String>,
/// Optional target triple to clean under `target/<triple>/...`.
target: Option<String>, target: Option<String>,
/// When true, also clean each workspace member that has a `target` directory.
all: bool, 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> { fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
let mut profile: Option<String> = None; let mut profile: Option<String> = None;
let mut target: Option<String> = None; let mut target: Option<String> = None;
@@ -105,8 +122,9 @@ fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
continue; continue;
} }
if arg.starts_with("--") { if arg.starts_with("--") {
// Accept only known profile names (e.g. debug/release), not arbitrary flags.
let candidate = arg.trim_start_matches("--").to_string(); let candidate = arg.trim_start_matches("--").to_string();
if flags(&candidate).is_some() { if !default_profile_flags(&candidate).is_empty() {
if profile.is_some() { if profile.is_some() {
return Err("Duplicate profile flag".to_string()); return Err("Duplicate profile flag".to_string());
} }
@@ -123,20 +141,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> { fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?; let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
let target = flags // Prefer CLI --target, then build.target from config, then host defaults on Linux/BSD.
.target let target = flags.target.clone().or_else(|| {
.clone()
.or_else(|| {
config config
.get("build.target") .get("build.target")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.map(|s| s.trim().to_string()) .map(|s| s.trim().to_string())
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
}) });
.or_else(|| Some(default_target_triple())); 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 if flags.all
&& let Some(workspace) = parse_workspace( && let Some(workspace) = parse_workspace(
@@ -147,12 +180,21 @@ fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
)? )?
{ {
for member in &workspace.members { for member in &workspace.members {
let member_target = member.path.join("target");
if member_target.is_dir() {
clean_project_at(&member.path, flags.profile.as_deref(), target.as_deref())?; clean_project_at(&member.path, flags.profile.as_deref(), target.as_deref())?;
} }
} }
}
clean_project_at(root, flags.profile.as_deref(), target.as_deref()) 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( fn clean_project_at(
project_root: &Path, project_root: &Path,
profile: Option<&str>, profile: Option<&str>,
@@ -180,6 +222,7 @@ fn clean_project_at(
}; };
let target_items = check_dir(Some("target")).unwrap_or_default(); let target_items = check_dir(Some("target")).unwrap_or_default();
let parent_dir = target.unwrap_or(""); 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() { let dir_exists = if target.is_some() {
target_items.contains(&parent_dir.to_string()) target_items.contains(&parent_dir.to_string())
} else { } else {
@@ -218,6 +261,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> { fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> {
let patterns = match config.get("build.clean") { let patterns = match config.get("build.clean") {
Some(v) => v Some(v) => v
@@ -227,7 +274,7 @@ fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> {
.filter_map(|item| item.as_str()) .filter_map(|item| item.as_str())
.map(|s| { .map(|s| {
let value = s.replace("{profile}", profile); let value = s.replace("{profile}", profile);
substitute_version_vars(&value, config) substitute_clean_vars(&value, config)
}) })
.collect::<Vec<String>>(), .collect::<Vec<String>>(),
None => Vec::new(), None => Vec::new(),
@@ -248,17 +295,15 @@ fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> {
Ok(()) Ok(())
} }
fn substitute_version_vars(value: &str, config: &Config) -> String { /// 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 let version = config
.get("package.version") .get("package.version")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or(""); .unwrap_or("");
let info = parse_version_info(version); let info = parse_version_info(version);
value crate::utils::build::substitute_version_vars(value, &info)
.replace("{version}", &info.full)
.replace("{version_major}", &info.major)
.replace("{version_minor}", &info.minor)
.replace("{version_patch}", &info.patch)
.replace("{version_suffix}", &info.suffix)
.replace("{version_suffix_dash}", &info.suffix_dash)
} }

View File

@@ -27,18 +27,32 @@ use std::time::{SystemTime, UNIX_EPOCH};
const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/dexoron/dcr/releases/latest"; 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)] #[derive(Debug, Deserialize)]
struct Release { struct Release {
/// Release tag, typically including a leading `v` (e.g. `v1.2.3`).
tag_name: String, tag_name: String,
/// Downloadable assets attached to the release.
assets: Vec<ReleaseAsset>, assets: Vec<ReleaseAsset>,
} }
/// A single downloadable asset from a GitHub release.
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct ReleaseAsset { struct ReleaseAsset {
/// Asset file name as published on GitHub.
name: String, name: String,
/// Direct URL used to download the asset.
browser_download_url: String, 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 { pub fn flag_update(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN); printc("USAGE:", BOLD_GREEN);
@@ -62,6 +76,7 @@ pub fn flag_update(args: &[String]) -> i32 {
} }
}; };
// Refuse self-update when the binary is owned by pacman/AUR.
if let Some(package_name) = pacman_owned_package(&current_exe) { if let Some(package_name) = pacman_owned_package(&current_exe) {
warn("This dcr binary is managed by pacman/AUR"); warn("This dcr binary is managed by pacman/AUR");
println!( println!(
@@ -81,6 +96,7 @@ pub fn flag_update(args: &[String]) -> i32 {
} }
}; };
// Normalize the tag by stripping a leading `v` before comparison.
let latest_version = release.tag_name.trim_start_matches('v'); let latest_version = release.tag_name.trim_start_matches('v');
if latest_version == current_version { if latest_version == current_version {
println!("Latest version is already installed: {current_version}"); println!("Latest version is already installed: {current_version}");
@@ -113,6 +129,7 @@ pub fn flag_update(args: &[String]) -> i32 {
} }
set_executable_permissions(&temp_path); 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() { if self_replace::self_replace(&temp_path).is_err() {
let _ = fs::remove_file(&temp_path); let _ = fs::remove_file(&temp_path);
error("Failed to replace current binary"); error("Failed to replace current binary");
@@ -124,6 +141,7 @@ pub fn flag_update(args: &[String]) -> i32 {
0 0
} }
/// Fetches the latest DCR release metadata from the GitHub Releases API.
fn fetch_latest_release() -> Result<Release, String> { fn fetch_latest_release() -> Result<Release, String> {
let response = ureq::get(LATEST_RELEASE_URL) let response = ureq::get(LATEST_RELEASE_URL)
.set("User-Agent", "dcr-updater") .set("User-Agent", "dcr-updater")
@@ -138,6 +156,7 @@ fn fetch_latest_release() -> Result<Release, String> {
.map_err(|_| "GitHub API response has an unexpected format".to_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> { fn download_asset(url: &str) -> Result<Vec<u8>, String> {
let response = ureq::get(url) let response = ureq::get(url)
.set("User-Agent", "dcr-updater") .set("User-Agent", "dcr-updater")
@@ -155,9 +174,11 @@ fn download_asset(url: &str) -> Result<Vec<u8>, String> {
Ok(data) Ok(data)
} }
/// Builds the list of expected asset file names for the given build target.
fn asset_candidates(target: &str) -> Vec<String> { fn asset_candidates(target: &str) -> Vec<String> {
let mut names = vec![format!("dcr-{target}")]; 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") { if target.contains("-windows-") || target.ends_with("-windows") {
names.push(format!("dcr-{target}.exe")); names.push(format!("dcr-{target}.exe"));
} }
@@ -165,6 +186,7 @@ fn asset_candidates(target: &str) -> Vec<String> {
names names
} }
/// Creates a unique temporary path next to the current executable for the download.
fn temp_binary_path(current_exe: &Path) -> PathBuf { fn temp_binary_path(current_exe: &Path) -> PathBuf {
let stamp = SystemTime::now() let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
@@ -177,6 +199,7 @@ fn temp_binary_path(current_exe: &Path) -> PathBuf {
current_exe.with_extension(extension) current_exe.with_extension(extension)
} }
/// Sets executable permissions on Unix; no-op on other platforms.
fn set_executable_permissions(_path: &Path) { fn set_executable_permissions(_path: &Path) {
#[cfg(unix)] #[cfg(unix)]
{ {
@@ -189,6 +212,7 @@ fn set_executable_permissions(_path: &Path) {
} }
} }
/// On Linux, returns the pacman package that owns `path`, if any.
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn pacman_owned_package(path: &Path) -> Option<String> { fn pacman_owned_package(path: &Path) -> Option<String> {
let output = Command::new("pacman").arg("-Qoq").arg(path).output().ok()?; let output = Command::new("pacman").arg("-Qoq").arg(path).output().ok()?;
@@ -204,6 +228,7 @@ fn pacman_owned_package(path: &Path) -> Option<String> {
Some(package_name) Some(package_name)
} }
/// Non-Linux stub: package-manager ownership is not checked.
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
fn pacman_owned_package(_path: &Path) -> Option<String> { fn pacman_owned_package(_path: &Path) -> Option<String> {
None None

View File

@@ -18,25 +18,69 @@
use crate::config::{PROFILE, flags}; use crate::config::{PROFILE, flags};
use crate::utils::log::warn; use crate::utils::log::warn;
/// Flags shared by the `build` and `run` CLI commands.
pub struct BuildRunFlags { pub struct BuildRunFlags {
/// Build profile name (for example `debug` or `release`).
pub profile: String, pub profile: String,
/// Optional target triple or platform name.
pub target: Option<String>, pub target: Option<String>,
/// Optional workspace member name (`--workspace <name>`).
pub workspace: Option<String>, pub workspace: Option<String>,
/// Force a rebuild even when artifacts appear up to date.
pub force: bool, pub force: bool,
/// Clean build outputs before building.
pub clean: bool, pub clean: bool,
/// Enable verbose logging during the build or run.
pub verbose: bool, 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> { 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 profile = PROFILE.to_string();
let mut target = None; let mut target = None;
let mut workspace = None; let mut workspace = None;
let mut force = false; let mut force = false;
let mut clean = false; let mut clean = false;
let mut verbose = false; let mut verbose = false;
let mut iter = args.iter(); let mut print_artifact_path = false;
let mut iter = dcr_args.iter();
while let Some(arg) = iter.next() { while let Some(arg) = iter.next() {
// DCR build/run options are long flags only (`--name`).
if !arg.starts_with("--") { if !arg.starts_with("--") {
warn("Unknown argument"); warn("Unknown argument");
return Err(1); return Err(1);
@@ -54,6 +98,10 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
verbose = true; verbose = true;
continue; continue;
} }
if candidate == "print-artifact-path" {
print_artifact_path = true;
continue;
}
if candidate == "workspace" { if candidate == "workspace" {
if let Some(w) = iter.next() { if let Some(w) = iter.next() {
workspace = Some(w.clone()); workspace = Some(w.clone());
@@ -72,6 +120,7 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
} }
continue; continue;
} }
// Profile names come from config::flags; only one profile may be set.
if flags(candidate).is_some() { if flags(candidate).is_some() {
if profile != PROFILE { if profile != PROFILE {
warn("Duplicate profile flag"); warn("Duplicate profile flag");
@@ -91,5 +140,43 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
force, force,
clean, clean,
verbose, 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

@@ -20,6 +20,12 @@ use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use glob::glob; use glob::glob;
use std::process::Command; 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 { pub fn fmt(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN); printc("USAGE:", BOLD_GREEN);
@@ -31,6 +37,7 @@ pub fn fmt(args: &[String]) -> i32 {
return 0; return 0;
} }
// Collect all matching files from src/ and tests/ using glob patterns
let patterns = [ let patterns = [
"src/**/*.c", "src/**/*.c",
"src/**/*.cpp", "src/**/*.cpp",
@@ -66,6 +73,7 @@ pub fn fmt(args: &[String]) -> i32 {
return 0; return 0;
} }
// Format the collected files
println!( println!(
" {} {} files", " {} {} files",
colored("Formatting", BOLD_GREEN), colored("Formatting", BOLD_GREEN),

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,12 @@
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc}; 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 { pub fn help() -> i32 {
println!("DCR (Dexoron Cargo Realization)"); println!("DCR (Dexoron Cargo Realization)");
println!("C project manager inspired by Cargo."); println!("C project manager inspired by Cargo.");
@@ -29,9 +35,12 @@ pub fn help() -> i32 {
println!(" init Initialize the current directory as a project"); println!(" init Initialize the current directory as a project");
println!(" build [--profile] Build the project (default: --debug)"); println!(" build [--profile] Build the project (default: --debug)");
println!(" run [--profile] Build and run 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!(" tree Display the dependency tree");
println!(" test Run project tests (alias: tests)"); println!(" test Run project tests (alias: tests)");
println!(" fmt Format all C/C++ source files using clang-format"); println!(" 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!(" clean Remove the target directory");
println!(" gen <subcommand> Generate IDE integration files"); println!(" gen <subcommand> Generate IDE integration files");
printc("FLAGS:", BOLD_GREEN); printc("FLAGS:", BOLD_GREEN);

View File

@@ -16,15 +16,18 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::config::FILE_MAIN_C; use crate::config::FILE_MAIN_C;
use crate::core::config::{Config, validate_package_name}; use crate::core::build_config::{Config, validate_package_name};
use crate::core::vcs::VcsKind; use crate::core::vcs::VcsKind;
use crate::utils::fs::check_dir; use crate::utils::fs::check_dir;
use crate::utils::log::{error, warn}; use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc}; use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::fs; use std::fs;
use std::io::Write; use std::io::Write;
use toml::Value;
/// 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 { pub fn init(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN); printc("USAGE:", BOLD_GREEN);
@@ -36,6 +39,7 @@ pub fn init(args: &[String]) -> i32 {
return 0; return 0;
} }
// Parse options (--vcs) and collect positional arguments.
let mut vcs_str = None; let mut vcs_str = None;
let mut clean_args = Vec::new(); let mut clean_args = Vec::new();
let mut iter = args.iter(); let mut iter = args.iter();
@@ -59,6 +63,7 @@ pub fn init(args: &[String]) -> i32 {
return 1; return 1;
} }
// Resolve target project name from current directory name and enforce empty directory check.
let items = check_dir(None).unwrap_or_default(); let items = check_dir(None).unwrap_or_default();
let project_name = std::env::current_dir() let project_name = std::env::current_dir()
.ok() .ok()
@@ -84,26 +89,18 @@ pub fn init(args: &[String]) -> i32 {
.unwrap_or_else(|_| ".".to_string()); .unwrap_or_else(|_| ".".to_string());
println!("Initializing the project in {cwd}"); println!("Initializing the project in {cwd}");
let mut config = match Config::new("./dcr.toml") { // Create and initialize default manifest (dcr.toml).
Ok(cfg) => cfg, if Config::create("./dcr.toml", Some(&project_name)).is_err() {
Err(_) => {
error("Failed to create dcr.toml"); error("Failed to create dcr.toml");
return 1; return 1;
} }
};
if config
.edit("package.name", Value::String(project_name.clone()))
.is_err()
{
error("Failed to write dcr.toml");
return 1;
}
println!( println!(
" {} Created file {}", " {} Created file {}",
colored("✔", BOLD_GREEN), colored("✔", BOLD_GREEN),
colored("dcr.toml", BOLD_CYAN) colored("dcr.toml", BOLD_CYAN)
); );
// Create src directory and write template main.c file.
if fs::create_dir("src").is_err() { if fs::create_dir("src").is_err() {
error("Failed to create src/"); error("Failed to create src/");
return 1; return 1;
@@ -125,7 +122,7 @@ pub fn init(args: &[String]) -> i32 {
colored("src/main.c", BOLD_CYAN) colored("src/main.c", BOLD_CYAN)
); );
// settings VCS // Resolve target VCS provider or detect existing repositories.
let mut vcs_kind = VcsKind::Git; let mut vcs_kind = VcsKind::Git;
if let Some(ref vcs_val) = vcs_str { if let Some(ref vcs_val) = vcs_str {
match VcsKind::parse(vcs_val) { match VcsKind::parse(vcs_val) {
@@ -159,6 +156,10 @@ pub fn init(args: &[String]) -> i32 {
} }
} }
if let Err(e) = crate::utils::fs::ensure_gitignore_has_dcr(std::path::Path::new(".")) {
warn(&format!("Failed to update .gitignore: {e}"));
}
println!( println!(
"Project `{}` successfully created\n", "Project `{}` successfully created\n",
colored(project_name.as_str(), BOLD_GREEN) colored(project_name.as_str(), BOLD_GREEN)

194
src/cli/lint.rs Normal file
View File

@@ -0,0 +1,194 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::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");
return 0;
}
let do_fix = args.iter().any(|a| a == "--fix");
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
error("Failed to determine current directory");
return 1;
}
};
let root = match find_project_root(&start_dir) {
Ok(Some(r)) => r,
Ok(None) => {
error("dcr.toml not found");
return 1;
}
Err(_) => {
error("Failed to find project root");
return 1;
}
};
let config = match Config::open(&root.join("dcr.toml").to_string_lossy()) {
Ok(c) => c,
Err(e) => {
error(&format!("Failed to load dcr.toml: {e}"));
return 1;
}
};
let profile = "debug";
let build_roots = get_list_with_profile(&config, "roots", profile);
let src_disable = get_bool_with_profile(&config, "src_disable", profile, false);
// Construct source roots from config
let mut source_roots: Vec<String> = Vec::new();
for raw in &build_roots {
let p = Path::new(raw);
if p.is_absolute() {
source_roots.push(raw.clone());
} else {
source_roots.push(root.join(p).to_string_lossy().to_string());
}
}
if !src_disable && source_roots.is_empty() {
source_roots.push(root.join("src").to_string_lossy().to_string());
}
let cxx_extensions = ["c", "cpp", "cxx", "cc"];
let exclude_dirs: Vec<PathBuf> = Vec::new();
let include_paths: Vec<String> = Vec::new();
let roots: Vec<PathBuf> = source_roots.iter().map(PathBuf::from).collect();
let mut files =
match common::collect_sources(&roots, &cxx_extensions, &exclude_dirs, &include_paths) {
Ok(f) => f,
Err(e) => {
error(&format!("Failed to collect source files: {e}"));
return 1;
}
};
let tests_dir = root.join("tests");
if tests_dir.is_dir() {
let test_roots = vec![tests_dir];
if let Ok(mut test_files) =
common::collect_sources(&test_roots, &cxx_extensions, &exclude_dirs, &include_paths)
{
files.append(&mut test_files);
}
}
files.sort();
if files.is_empty() {
println!(" {} No files to lint", colored("Lint", BOLD_GREEN));
return 0;
}
println!(
" {} {} files {}",
colored("Linting", BOLD_GREEN),
files.len(),
if do_fix { "(with --fix)" } else { "" }
);
let mut total_issues = 0u32;
let mut failed = 0u32;
// Execute clang-tidy for each file and count issues
for file in &files {
let mut cmd = Command::new("clang-tidy");
if do_fix {
cmd.arg("--fix");
}
cmd.arg("--quiet");
cmd.arg(file);
let output = match cmd.output() {
Ok(o) => o,
Err(e) => {
error(&format!("Failed to execute clang-tidy: {}", e));
return 1;
}
};
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let combined = format!("{}{}", stdout, stderr);
let issues: Vec<&str> = combined
.lines()
.filter(|l| l.contains("warning:") || l.contains("error:"))
.collect();
let issue_count = issues.len();
if issue_count > 0 {
total_issues += issue_count as u32;
failed += 1;
print!(" {} ", colored("×", BOLD_YELLOW));
println!("{} ({} issues)", file, issue_count);
for issue in &issues {
println!(" {}", issue);
}
} else {
print!(" {} ", colored("✓", BOLD_GREEN));
println!("{}", file);
}
}
let total = files.len();
let passed = total - failed as usize;
if failed == 0 {
println!(
" {} {} files, no issues",
colored("Lint", BOLD_GREEN),
total
);
} else {
println!(
" {} {}/{} files passed, {} issues found",
colored("Lint", BOLD_YELLOW),
passed,
total,
total_issues
);
}
0
}

View File

@@ -15,6 +15,9 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
/// DCR CLI module.
///
/// This module declares submodules for all DCR commands.
pub mod add; pub mod add;
pub mod build; pub mod build;
pub mod clean; pub mod clean;
@@ -24,6 +27,7 @@ pub mod fmt;
pub mod r#gen; pub mod r#gen;
pub mod help; pub mod help;
pub mod init; pub mod init;
pub mod lint;
pub mod new; pub mod new;
pub mod run; pub mod run;
pub mod setup; pub mod setup;

View File

@@ -16,15 +16,18 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::config::FILE_MAIN_C; use crate::config::FILE_MAIN_C;
use crate::core::config::{Config, validate_package_name}; use crate::core::build_config::{Config, validate_package_name};
use crate::core::vcs::VcsKind; use crate::core::vcs::VcsKind;
use crate::utils::fs::check_dir; use crate::utils::fs::check_dir;
use crate::utils::log::{error, warn}; use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc}; use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::fs; use std::fs;
use std::io::Write; use std::io::Write;
use toml::Value;
/// 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 { pub fn new(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN); printc("USAGE:", BOLD_GREEN);
@@ -38,6 +41,7 @@ pub fn new(args: &[String]) -> i32 {
let items = check_dir(None).unwrap_or_default(); 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 vcs_str = None;
let mut clean_args = Vec::new(); let mut clean_args = Vec::new();
let mut iter = args.iter(); let mut iter = args.iter();
@@ -85,7 +89,7 @@ pub fn new(args: &[String]) -> i32 {
"Directory `{}` already exists\n", "Directory `{}` already exists\n",
colored(project_name, BOLD_CYAN) colored(project_name, BOLD_CYAN)
)); ));
printc("Подсказка:", BOLD_CYAN); printc("Hint:", BOLD_CYAN);
println!( println!(
" Use `{}` to initialize an existing project\n or specify a different project name", " Use `{}` to initialize an existing project\n or specify a different project name",
colored("dcr init", BOLD_CYAN) colored("dcr init", BOLD_CYAN)
@@ -103,27 +107,19 @@ pub fn new(args: &[String]) -> i32 {
project_name project_name
); );
// Initialize and write project configuration manifest (dcr.toml).
let toml_path = format!("./{project_name}/dcr.toml"); let toml_path = format!("./{project_name}/dcr.toml");
let mut config = match Config::new(&toml_path) { if Config::create(&toml_path, Some(project_name)).is_err() {
Ok(cfg) => cfg,
Err(_) => {
error("Failed to create dcr.toml"); error("Failed to create dcr.toml");
return 1; return 1;
} }
};
if config
.edit("package.name", Value::String(project_name.to_string()))
.is_err()
{
error("Failed to write dcr.toml");
return 1;
}
println!( println!(
" {} Created file {}", " {} Created file {}",
colored("✔", BOLD_GREEN), colored("✔", BOLD_GREEN),
colored("dcr.toml", BOLD_CYAN) colored("dcr.toml", BOLD_CYAN)
); );
// Generate src directory and populate initial main.c file.
if fs::create_dir_all(format!("./{project_name}/src")).is_err() { if fs::create_dir_all(format!("./{project_name}/src")).is_err() {
error("Failed to create src/"); error("Failed to create src/");
return 1; return 1;
@@ -146,7 +142,7 @@ pub fn new(args: &[String]) -> i32 {
colored("src/main.c", BOLD_CYAN) colored("src/main.c", BOLD_CYAN)
); );
// settings VCS // Resolve target VCS mode based on CLI flags or parent directory state.
let mut vcs_kind = VcsKind::Git; let mut vcs_kind = VcsKind::Git;
if let Some(ref vcs_val) = vcs_str { if let Some(ref vcs_val) = vcs_str {
match VcsKind::parse(vcs_val) { match VcsKind::parse(vcs_val) {
@@ -180,6 +176,11 @@ pub fn new(args: &[String]) -> i32 {
} }
} }
let project_path = std::path::Path::new(project_name);
if let Err(e) = crate::utils::fs::ensure_gitignore_has_dcr(project_path) {
warn(&format!("Failed to update .gitignore: {e}"));
}
println!( println!(
"Project `{}` successfully created\n", "Project `{}` successfully created\n",
colored(project_name, BOLD_GREEN) colored(project_name, BOLD_GREEN)

View File

@@ -17,9 +17,9 @@
use crate::cli::build::build; use crate::cli::build::build;
use crate::cli::flags::parse_build_run_flags; use crate::cli::flags::parse_build_run_flags;
use crate::core::config::Config; use crate::core::build_config::Config;
use crate::core::runner::run_binary; use crate::core::runner::run_binary;
use crate::utils::build::{default_target_triple, normalize_target_os, parse_version_info}; use crate::utils::build::{normalize_target_os, parse_version_info, substitute_vars};
use crate::utils::fs::find_project_root; use crate::utils::fs::find_project_root;
use crate::utils::fs::with_dir; use crate::utils::fs::with_dir;
use crate::utils::log::error; use crate::utils::log::error;
@@ -27,6 +27,8 @@ use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
/// Retrieves the run command from the config, preferring target-specific,
/// then profile-specific, then the base `run.cmd`.
fn get_run_cmd( fn get_run_cmd(
config: &Config, config: &Config,
profile: &str, profile: &str,
@@ -55,16 +57,25 @@ 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 { pub fn run(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN); printc("USAGE:", BOLD_GREEN);
printc( printc(
" dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose]", " dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [-- <args>...]",
BOLD_CYAN, BOLD_CYAN,
); );
println!(); println!();
printc("DESCRIPTION:", BOLD_GREEN); printc("DESCRIPTION:", BOLD_GREEN);
println!(" Builds and runs the project. Only available for kind = \"bin\"."); println!(" Builds and runs the project. Only available for kind = \"bin\".");
println!(" Arguments after `--` are passed to the built binary (cargo-style).");
println!(); println!();
printc("OPTIONS:", BOLD_GREEN); printc("OPTIONS:", BOLD_GREEN);
println!(" --debug Run with debug profile (default)"); println!(" --debug Run with debug profile (default)");
@@ -73,6 +84,7 @@ pub fn run(args: &[String]) -> i32 {
println!(" --force Force a full rebuild"); println!(" --force Force a full rebuild");
println!(" --clean Clean before building"); println!(" --clean Clean before building");
println!(" --verbose Print detailed build output"); println!(" --verbose Print detailed build output");
println!(" -- <args>... Arguments forwarded to the binary");
return 0; return 0;
} }
@@ -108,8 +120,21 @@ pub fn run(args: &[String]) -> i32 {
Err(_) => return 1, Err(_) => return 1,
}; };
// Handle workspace-only root: delegate to workspace member
if config.is_workspace_only() { if config.is_workspace_only() {
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 {
let display = display_run_cmd(&cmd, &flags.bin_args);
println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
display
);
return run_shell_with_args(&cmd, &flags.bin_args);
}
return build_status;
}
let ws = match crate::core::workspace::parse_workspace( let ws = match crate::core::workspace::parse_workspace(
&config, &config,
&flags.profile, &flags.profile,
@@ -162,6 +187,7 @@ pub fn run(args: &[String]) -> i32 {
} }
} }
/// Builds and runs a single project at `root` (optionally as a workspace member).
fn run_project( fn run_project(
root: &Path, root: &Path,
flags: &crate::cli::flags::BuildRunFlags, flags: &crate::cli::flags::BuildRunFlags,
@@ -175,10 +201,13 @@ fn run_project(
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or(""); .unwrap_or("");
// If no target specified, use default host target for target-specific config
let mut target = flags.target.clone(); let mut target = flags.target.clone();
if target.is_none() { if target.is_none() {
target = Some(default_target_triple()); let bt =
crate::cli::build::get_build_string_with_profile(&config, "target", &flags.profile);
if !bt.is_empty() {
target = Some(bt);
}
} }
let build_kind = config let build_kind = config
@@ -189,19 +218,17 @@ fn run_project(
let out_dir = let out_dir =
crate::cli::build::get_build_string_with_profile(&config, "out_dir", &flags.profile); crate::cli::build::get_build_string_with_profile(&config, "out_dir", &flags.profile);
let normalized_target_dir = if !out_dir.is_empty() { let build_target_str = target.clone().unwrap_or_default();
Some(out_dir) let has_explicit = target.as_ref().is_some_and(|t| !t.trim().is_empty());
} else {
match workspace_root { let normalized_target_dir = Some(crate::utils::build::resolve_artifact_target_dir(
Some(wr) => target root,
.as_ref() workspace_root,
.and_then(|t| crate::cli::build::normalize_target(t, &flags.profile)) &flags.profile,
.map(|rel| wr.join(&rel).to_string_lossy().to_string()), &build_target_str,
None => target &out_dir,
.as_ref() has_explicit,
.and_then(|t| crate::cli::build::normalize_target(t, &flags.profile)), ));
}
};
let version = config let version = config
.get("package.version") .get("package.version")
@@ -211,12 +238,23 @@ fn run_project(
let kind = build_kind.trim(); let kind = build_kind.trim();
if run_cmd.is_none() if run_cmd.is_none()
&& (kind == "staticlib" || kind == "sharedlib" || kind == "efi" || kind == "elf") && (kind == "staticlib"
|| kind == "sharedlib"
|| kind == "efi"
|| kind == "elf"
|| kind == "flat-bin")
{ {
return Err("Cannot run library build".to_string()); return Err("Cannot run library build".to_string());
} }
let build_status = build(&args_for_build(flags)); let build_status = if let Some(wroot) = workspace_root {
with_dir(wroot, || {
Ok(build(&args_for_workspace_build(flags, project_name)))
})
.unwrap_or(1)
} else {
build(&args_for_build(flags))
};
let bin_path = crate::platform::bin_path( let bin_path = crate::platform::bin_path(
&flags.profile, &flags.profile,
project_name, project_name,
@@ -224,26 +262,36 @@ fn run_project(
); );
if build_status == 0 { if build_status == 0 {
if let Some(cmd) = run_cmd { if let Some(cmd) = run_cmd {
println!("\n {} {}", colored("Running", BOLD_GREEN), cmd); let display = display_run_cmd(&cmd, &flags.bin_args);
println!("--------------------------------"); println!(
return Ok(run_shell(&cmd)); " {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
display
);
return Ok(run_shell_with_args(&cmd, &flags.bin_args));
} }
println!("\n {} {}", colored("Running", BOLD_GREEN), bin_path); let display = display_bin_run(&bin_path, &flags.bin_args);
println!("--------------------------------"); println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
display
);
return Ok(run_binary( return Ok(run_binary(
project_name, project_name,
&flags.profile, &flags.profile,
normalized_target_dir.as_deref(), normalized_target_dir.as_deref(),
&flags.bin_args,
)); ));
} }
let fallback_code = if let Some(cmd) = run_cmd { let fallback_code = if let Some(cmd) = run_cmd {
run_shell(&cmd) run_shell_with_args(&cmd, &flags.bin_args)
} else { } else {
run_binary( run_binary(
project_name, project_name,
&flags.profile, &flags.profile,
normalized_target_dir.as_deref(), normalized_target_dir.as_deref(),
&flags.bin_args,
) )
}; };
if fallback_code != 1 { if fallback_code != 1 {
@@ -253,6 +301,7 @@ fn run_project(
Err("Fix errors in the code to run the project".to_string()) 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> { fn args_for_build(flags: &crate::cli::flags::BuildRunFlags) -> Vec<String> {
let mut args = Vec::new(); let mut args = Vec::new();
args.push(format!("--{}", flags.profile)); args.push(format!("--{}", flags.profile));
@@ -264,12 +313,44 @@ fn args_for_build(flags: &crate::cli::flags::BuildRunFlags) -> Vec<String> {
args.push("--workspace".to_string()); args.push("--workspace".to_string());
args.push(name.clone()); args.push(name.clone());
} }
if flags.force {
args.push("--force".to_string());
}
if flags.clean {
args.push("--clean".to_string());
}
if flags.verbose { if flags.verbose {
args.push("--verbose".to_string()); args.push("--verbose".to_string());
} }
args args
} }
/// Constructs build arguments specifically for a workspace member.
fn args_for_workspace_build(
flags: &crate::cli::flags::BuildRunFlags,
member_name: &str,
) -> Vec<String> {
let mut args = Vec::new();
args.push(format!("--{}", flags.profile));
if let Some(ref target) = flags.target {
args.push("--target".to_string());
args.push(target.clone());
}
args.push("--workspace".to_string());
args.push(member_name.to_string());
if flags.force {
args.push("--force".to_string());
}
if flags.clean {
args.push("--clean".to_string());
}
if flags.verbose {
args.push("--verbose".to_string());
}
args
}
/// Executes the given shell command and returns its exit code.
fn run_shell(cmd: &str) -> i32 { fn run_shell(cmd: &str) -> i32 {
let status = if cfg!(target_os = "windows") { let status = if cfg!(target_os = "windows") {
Command::new("cmd").arg("/C").arg(cmd).status() Command::new("cmd").arg("/C").arg(cmd).status()
@@ -283,13 +364,68 @@ 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 { fn substitute_run_vars(cmd: &str, profile: &str, version: &str) -> String {
let info = parse_version_info(version); let info = parse_version_info(version);
cmd.replace("{profile}", profile) substitute_vars(cmd, &info, profile, "")
.replace("{version}", &info.full)
.replace("{version_major}", &info.major)
.replace("{version_minor}", &info.minor)
.replace("{version_patch}", &info.patch)
.replace("{version_suffix}", &info.suffix)
.replace("{version_suffix_dash}", &info.suffix_dash)
} }

View File

@@ -17,6 +17,7 @@
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc}; 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 { pub fn setup(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN); printc("USAGE:", BOLD_GREEN);
@@ -29,6 +30,7 @@ pub fn setup(args: &[String]) -> i32 {
} }
println!("Setting up DCR registries..."); println!("Setting up DCR registries...");
// Load and process the registries, returning 0 on success or 1 on error
match crate::core::registry::RegistryManager::load() { match crate::core::registry::RegistryManager::load() {
Ok(manager) => { Ok(manager) => {
println!("Loaded {} registries.", manager.config.registry.len()); println!("Loaded {} registries.", manager.config.registry.len());

View File

@@ -17,7 +17,7 @@
use crate::cli::build; use crate::cli::build;
use crate::config::{FILE_DCR_TEST_H, FILE_TEST_C, flags}; use crate::config::{FILE_DCR_TEST_H, FILE_TEST_C, flags};
use crate::core::config::Config; use crate::core::build_config::Config;
use crate::utils::build::{ use crate::utils::build::{
get_config_opt, get_config_str, get_language_with_profile, get_list_with_profile, get_config_opt, get_config_str, get_language_with_profile, get_list_with_profile,
get_string_with_profile, resolve_compiler, resolve_pkg_config_flags, get_string_with_profile, resolve_compiler, resolve_pkg_config_flags,
@@ -31,6 +31,7 @@ use std::process::Command;
const BOLD_BLUE: &str = "\x1b[1m\x1b[94m"; const BOLD_BLUE: &str = "\x1b[1m\x1b[94m";
/// Runs project tests and prints a unified testsuite report.
pub fn test(args: &[String]) -> i32 { pub fn test(args: &[String]) -> i32 {
let mut init_header = false; let mut init_header = false;
let mut profile = "debug"; let mut profile = "debug";
@@ -101,6 +102,7 @@ pub fn test(args: &[String]) -> i32 {
} }
} }
/// Executes the testsuite for the given build profile.
fn run_testsuite(profile: &str) -> Result<i32, String> { fn run_testsuite(profile: &str) -> Result<i32, String> {
if build::build(&[format!("--{profile}")]) != 0 { if build::build(&[format!("--{profile}")]) != 0 {
return Ok(1); return Ok(1);
@@ -132,8 +134,12 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
cflags.extend(get_list_with_profile(&config, "cflags", profile)); cflags.extend(get_list_with_profile(&config, "cflags", profile));
let ldflags = get_list_with_profile(&config, "ldflags", profile); let ldflags = get_list_with_profile(&config, "ldflags", profile);
let pkg_configs = get_list_with_profile(&config, "pkg_config", profile); let pkg_configs = get_list_with_profile(&config, "pkg_config", profile);
let (cflags, ldflags) = resolve_pkg_config_flags(&pkg_configs, &cflags, &ldflags)?; let (mut cflags, mut ldflags) = resolve_pkg_config_flags(&pkg_configs, &cflags, &ldflags)?;
let includes = test_include_dirs(&config, profile); 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()?; let test_sources = collect_test_sources()?;
if test_sources.is_empty() { if test_sources.is_empty() {
return Err("tests/test.c not found; run 'dcr test --init' first".to_string()); return Err("tests/test.c not found; run 'dcr test --init' first".to_string());
@@ -260,6 +266,7 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
Ok(0) Ok(0)
} }
/// Collects all .c source files from the tests/ directory.
fn collect_test_sources() -> Result<Vec<PathBuf>, String> { fn collect_test_sources() -> Result<Vec<PathBuf>, String> {
let tests_dir = Path::new("./tests"); let tests_dir = Path::new("./tests");
if !tests_dir.is_dir() { if !tests_dir.is_dir() {
@@ -278,6 +285,7 @@ fn collect_test_sources() -> Result<Vec<PathBuf>, String> {
Ok(sources) Ok(sources)
} }
/// Returns a list of include directories for test compilation.
fn test_include_dirs(config: &Config, profile: &str) -> Vec<String> { fn test_include_dirs(config: &Config, profile: &str) -> Vec<String> {
let mut dirs = vec![ let mut dirs = vec![
"./tests".to_string(), "./tests".to_string(),
@@ -288,6 +296,7 @@ fn test_include_dirs(config: &Config, profile: &str) -> Vec<String> {
dirs dirs
} }
/// Compiles a single test source file to an object file.
fn compile_test_source( fn compile_test_source(
compiler: &str, compiler: &str,
standard: &str, standard: &str,
@@ -317,6 +326,7 @@ fn compile_test_source(
Ok(()) Ok(())
} }
/// Links the test object file into an executable binary.
fn link_test_binary( fn link_test_binary(
compiler: &str, compiler: &str,
ldflags: &[String], ldflags: &[String],
@@ -343,6 +353,7 @@ fn link_test_binary(
Ok(()) Ok(())
} }
/// Prints the stdout and stderr from a command execution.
fn print_command_output(output: &std::process::Output) { fn print_command_output(output: &std::process::Output) {
if !output.stdout.is_empty() { if !output.stdout.is_empty() {
print!("{}", String::from_utf8_lossy(&output.stdout)); print!("{}", String::from_utf8_lossy(&output.stdout));
@@ -352,6 +363,7 @@ fn print_command_output(output: &std::process::Output) {
} }
} }
/// Creates the default test files (dcr_test.h and test.c) in the tests/ directory.
fn ensure_test_header() -> Result<(), String> { fn ensure_test_header() -> Result<(), String> {
fs::create_dir_all("./tests").map_err(|_| "Failed to create tests/".to_string())?; fs::create_dir_all("./tests").map_err(|_| "Failed to create tests/".to_string())?;
fs::write("./tests/dcr_test.h", FILE_DCR_TEST_H) fs::write("./tests/dcr_test.h", FILE_DCR_TEST_H)
@@ -363,6 +375,7 @@ fn ensure_test_header() -> Result<(), String> {
Ok(()) Ok(())
} }
/// Extracts test case names from a C source file by parsing TEST_CASE macros.
fn extract_test_names_path(path: &Path) -> Vec<String> { fn extract_test_names_path(path: &Path) -> Vec<String> {
let Ok(content) = fs::read_to_string(path) else { let Ok(content) = fs::read_to_string(path) else {
return Vec::new(); return Vec::new();
@@ -379,6 +392,7 @@ fn extract_test_names_path(path: &Path) -> Vec<String> {
names names
} }
/// Prints a test result field with color coding.
fn print_field(label: &str, value: i32, color: &str) { fn print_field(label: &str, value: i32, color: &str) {
if value == 0 { if value == 0 {
println!("{}: {}", label, value); println!("{}: {}", label, value);

View File

@@ -15,13 +15,17 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::config::Config; use crate::core::build_config::Config;
use crate::utils::log::error; use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc}; use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use std::collections::HashSet; use std::collections::HashSet;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use toml::Value; 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 { pub fn tree(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN); printc("USAGE:", BOLD_GREEN);
@@ -62,6 +66,9 @@ pub fn tree(args: &[String]) -> i32 {
0 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( fn print_deps(
deps: &toml::value::Table, deps: &toml::value::Table,
base_path: &Path, base_path: &Path,
@@ -81,6 +88,7 @@ fn print_deps(
let mut sub_deps = None; let mut sub_deps = None;
let mut resolved_path = None; let mut resolved_path = None;
// Resolve dependency path if it's a path-based dependency
if let Some(path) = &dep_path { if let Some(path) = &dep_path {
let dcr_toml = path.join("dcr.toml"); let dcr_toml = path.join("dcr.toml");
if dcr_toml.exists() if dcr_toml.exists()
@@ -107,6 +115,19 @@ fn print_deps(
format!(" v{}", v) format!(" v{}", v)
} else if let Some(git) = t.get("git").and_then(|v| v.as_str()) { } else if let Some(git) = t.get("git").and_then(|v| v.as_str()) {
format!(" ({})", git) 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 { } else {
"".to_string() "".to_string()
} }
@@ -117,6 +138,7 @@ fn print_deps(
println!("{}{}{}{}", prefix, connector, name, version); println!("{}{}{}{}", prefix, connector, name, version);
// Recurse only if sub-dependencies exist and this dep hasn't been seen
if let Some(s_deps) = sub_deps if let Some(s_deps) = sub_deps
&& !seen.contains(*name) && !seen.contains(*name)
{ {
@@ -130,6 +152,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> { fn resolve_dep_path(value: &Value, base_path: &Path) -> Option<PathBuf> {
match value { match value {
Value::String(s) => s Value::String(s) => s

View File

@@ -96,17 +96,8 @@ 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]> { pub fn flags(profile: &str) -> Option<&'static [&'static str]> {
match profile { let f = crate::utils::build::default_profile_flags(profile);
"release" => Some(&["-O3", "-DNDEBUG"]), if f.is_empty() { None } else { Some(f) }
"debug" => Some(&[
"-O0",
"-g",
"-Wall",
"-Wextra",
"-fno-omit-frame-pointer",
"-DDCR_DEBUG",
]),
_ => None,
}
} }

276
src/core/build/archive.rs Normal file
View File

@@ -0,0 +1,276 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build_config::ArchiveConfig;
use fatfs::{FileSystem, FormatVolumeOptions, format_volume};
use glob::glob;
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,
}
impl<T: Read> Read for OffsetFile<T> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.inner.read(buf)
}
}
impl<T: Write> Write for OffsetFile<T> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.inner.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
impl<T: Seek> Seek for OffsetFile<T> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
match pos {
SeekFrom::Start(n) => {
let actual = self.inner.seek(SeekFrom::Start(n + self.offset))?;
if actual < self.offset {
Ok(0)
} else {
Ok(actual - self.offset)
}
}
SeekFrom::Current(n) => {
let actual = self.inner.seek(SeekFrom::Current(n))?;
if actual < self.offset {
Ok(0)
} else {
Ok(actual - self.offset)
}
}
SeekFrom::End(n) => {
let actual = self.inner.seek(SeekFrom::End(n))?;
if actual < self.offset {
Ok(0)
} else {
Ok(actual - self.offset)
}
}
}
}
}
/// 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() {
return Ok(0);
}
let mut num_str = String::new();
let mut unit = String::new();
for c in s.chars() {
if c.is_ascii_digit() || c == '.' {
num_str.push(c);
} else {
unit.push(c);
}
}
let val: f64 = num_str
.parse()
.map_err(|_| format!("invalid size number: {num_str}"))?;
let unit = unit.trim().to_lowercase();
let multiplier = match unit.as_str() {
"k" | "kb" => 1024.0,
"m" | "mb" => 1024.0 * 1024.0,
"g" | "gb" => 1024.0 * 1024.0 * 1024.0,
"" => 1.0,
_ => return Err(format!("unknown unit: {unit}")),
};
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,
profile: &str,
) -> Result<(), String> {
let output_substituted = config.output.replace("{profile}", profile);
let output_path = project_root.join(&output_substituted);
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("failed to create output parent dir: {e}"))?;
}
let size = if let Some(sz_str) = &config.size {
parse_size(sz_str)?
} else {
1474560
};
let offset = if let Some(off_str) = &config.offset {
parse_size(off_str)?
} else {
0
};
let mut file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&output_path)
.map_err(|e| format!("failed to open output file: {e}"))?;
let current_len = file.metadata().map(|m| m.len()).unwrap_or(0);
if current_len < size {
file.set_len(size)
.map_err(|e| format!("failed to set image file size: {e}"))?;
}
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() {
let mut boot_file =
File::open(&boot_path).map_err(|e| format!("failed to open bootsector: {e}"))?;
let mut boot_bytes = vec![0; 512];
let _ = boot_file
.read(&mut boot_bytes)
.map_err(|e| format!("failed to read bootsector: {e}"))?;
file.seek(SeekFrom::Start(0))
.map_err(|e| format!("failed to seek to start: {e}"))?;
file.write_all(&boot_bytes)
.map_err(|e| format!("failed to write bootsector: {e}"))?;
}
}
file.seek(SeekFrom::Start(0))
.map_err(|e| format!("failed to seek to start: {e}"))?;
let mut wrapper = OffsetFile {
inner: file,
offset,
};
let label = config.label.as_deref().unwrap_or("VOLUME");
let mut label_bytes = [b' '; 11];
let bytes_to_copy = label.len().min(11);
label_bytes[..bytes_to_copy].copy_from_slice(&label.as_bytes()[..bytes_to_copy]);
let fat_type = match config.format.to_lowercase().as_str() {
"fat12" => Some(fatfs::FatType::Fat12),
"fat16" => Some(fatfs::FatType::Fat16),
"fat32" => Some(fatfs::FatType::Fat32),
_ => None,
};
let mut options = FormatVolumeOptions::new().volume_label(label_bytes);
if let Some(t) = fat_type {
options = options.fat_type(t);
}
format_volume(&mut wrapper, options)
.map_err(|e| format!("failed to format FAT volume: {e}"))?;
wrapper
.seek(SeekFrom::Start(0))
.map_err(|e| format!("failed to seek to start after format: {e}"))?;
let fs = FileSystem::new(wrapper, fatfs::FsOptions::new())
.map_err(|e| format!("failed to mount FAT fs: {e}"))?;
let root_dir = fs.root_dir();
for layout in &config.layout {
let from_substituted = layout.from.replace("{profile}", profile);
let from_pattern = project_root
.join(&from_substituted)
.to_string_lossy()
.to_string();
let paths = if from_pattern.contains('*') || from_pattern.contains('?') {
let mut matched = Vec::new();
if let Ok(entries) = glob(&from_pattern) {
for path in entries.flatten() {
matched.push(path);
}
}
matched
} else {
let p = project_root.join(&from_substituted);
if p.exists() { vec![p] } else { Vec::new() }
};
for path in paths {
if !path.is_file() {
continue;
}
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if filename.is_empty() {
continue;
}
let mut target_filename = filename.to_string();
let mut target_dir_str = layout.to.trim_matches('/');
let is_glob = from_pattern.contains('*') || from_pattern.contains('?');
if !is_glob
&& !layout.to.ends_with('/')
&& let Some(last_part) = Path::new(&layout.to).file_name().and_then(|n| n.to_str())
&& last_part.contains('.')
{
target_filename = last_part.to_string();
let parent_path = Path::new(&layout.to).parent().unwrap_or(Path::new(""));
target_dir_str = parent_path.to_str().unwrap_or("").trim_matches('/');
}
let mut current_dir = root_dir.clone();
if !target_dir_str.is_empty() {
let parts = target_dir_str.split('/');
for part in parts {
if part.is_empty() {
continue;
}
current_dir = match current_dir.open_dir(part) {
Ok(dir) => dir,
Err(_) => current_dir
.create_dir(part)
.map_err(|e| format!("failed to create dir {part}: {e}"))?,
};
}
}
let mut out_file = current_dir
.create_file(&target_filename)
.map_err(|e| format!("failed to create file {target_filename}: {e}"))?;
let mut in_file =
File::open(&path).map_err(|e| format!("failed to open input file: {e}"))?;
let mut buffer = Vec::new();
in_file
.read_to_end(&mut buffer)
.map_err(|e| format!("failed to read input file: {e}"))?;
out_file
.write_all(&buffer)
.map_err(|e| format!("failed to write to FAT file {target_filename}: {e}"))?;
}
}
Ok(())
}

View File

@@ -0,0 +1,426 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::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(),
None => Path::new("./target").join(ctx.profile),
}
}
/// 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");
let file = if ext.is_empty() {
name.to_string()
} else {
format!("{name}.{ext}")
};
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()
.and_then(|s| s.to_str())
.unwrap_or("out");
let ext = ctx.output_extension.unwrap_or("bin");
let file = if ext.is_empty() {
stem.to_string()
} else {
format!("{stem}.{ext}")
};
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)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
return Ok(candidate.to_string());
}
}
Err(
"flat-bin requires llvm-objcopy, objcopy, or gobjcopy in PATH \
(install binutils or LLVM tools)"
.to_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}"))?;
}
if !common::needs_rebuild(input, output) {
return Ok(());
}
let tool = resolve_objcopy()?;
let mut cmd = Command::new(&tool);
cmd.arg("-O").arg("binary").arg(input).arg(output);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
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],
default_linker: &str,
start_time: Instant,
) -> Result<f64, String> {
let out_path = flat_output_path(ctx);
if !common::needs_link(objects, &out_path) {
return Ok(common::elapsed_secs(start_time));
}
let intermediate = {
let dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
std::fs::create_dir_all(&dir).map_err(|e| format!("obj dir error: {e}"))?;
dir.join(format!("{}.flat.elf", ctx.project_name))
.to_string_lossy()
.to_string()
};
let mut cmd = Command::new(ctx.linker.unwrap_or(default_linker));
cmd.arg("-nostdlib").arg("-static");
for obj in objects {
cmd.arg(obj);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("-L{dir}"));
}
for lib in ctx.libs {
cmd.arg(format!("-l{lib}"));
}
for flag in ctx.ldflags {
cmd.arg(flag);
}
cmd.arg("-o").arg(&intermediate);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
let program = cmd.get_program().to_string_lossy().into_owned();
let output = cmd.output().map_err(|err| {
if err.kind() == std::io::ErrorKind::NotFound {
format!("linker not found: {program} (check [toolchain].ld / PATH)")
} else {
format!("flat-bin link failed: {err}")
}
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
return Err(format!(
"flat-bin link failed:\nstdout: {stdout}\nstderr: {stderr}"
));
}
objcopy_binary(ctx, &intermediate, &out_path)?;
Ok(common::elapsed_secs(start_time))
}
/// Archives objects into a static library.
pub(crate) fn archive_static(
ctx: &BuildContext,
objects: &[String],
start_time: Instant,
) -> Result<f64, String> {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
if !common::needs_link(objects, &lib_path) {
return Ok(common::elapsed_secs(start_time));
}
let archiver = ctx
.archiver
.unwrap_or_else(|| default_archiver(ctx.compiler));
let mut cmd = Command::new(archiver);
if cfg!(target_os = "windows") && is_msvc_lib(archiver) {
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
} else {
cmd.arg("rcs").arg(&lib_path);
}
for obj in objects {
cmd.arg(obj);
}
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],
default_linker: &str,
start_time: Instant,
) -> Result<f64, String> {
if crate::utils::build::is_flat_bin(ctx.kind) {
return link_flat_binary(ctx, objects, default_linker, start_time);
}
let mut cmd = Command::new(ctx.linker.unwrap_or(default_linker));
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
if ctx.kind == "efi" {
cmd.arg("-shared");
cmd.arg("-nostdlib");
cmd.arg("-Wl,-dll");
cmd.arg("-Wl,--subsystem,10");
} else if ctx.freestanding || is_bare_metal_target(ctx.target) {
cmd.arg("-nostdlib");
cmd.arg("-static");
}
for obj in objects {
cmd.arg(obj);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("-L{dir}"));
}
for lib in ctx.libs {
cmd.arg(format!("-l{lib}"));
}
for flag in ctx.ldflags {
cmd.arg(flag);
}
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
format!("{}.{}", name, ext)
};
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "efi" {
platform::efi_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "elf" {
platform::elf_path(ctx.profile, &final_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
};
if !common::needs_link(objects, &out_path) {
return Ok(common::elapsed_secs(start_time));
}
cmd.arg("-o").arg(out_path);
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);
}
let program = cmd.get_program().to_string_lossy().into_owned();
let output = cmd.output().map_err(|err| {
let msg = err.to_string();
if err.kind() == std::io::ErrorKind::NotFound {
format!("linker not found: {program} (check [toolchain].ld / PATH)")
} else {
format!("Build failed: {msg}")
}
})?;
if output.status.success() {
Ok(common::elapsed_secs(start_time))
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
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

@@ -0,0 +1,236 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::builder::artifact;
use crate::core::build::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());
}
let start_time = Instant::now();
let obj_dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
let qt_include_path = if ctx.qt {
crate::core::build::language::cxx::qt::process_qt(ctx, &obj_dir)?
} else {
None
};
let mut all_source_roots = ctx.source_roots.to_vec();
if let Some(qt_path) = &qt_include_path {
all_source_roots.push(qt_path.clone());
}
let extensions = source_extensions(ctx.language);
let sources = common::collect_sources(
&all_source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)?;
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "o", qt_include_path)?;
if is_compile_only(ctx.kind) && !is_flat_bin(ctx.kind) {
return Ok(common::elapsed_secs(start_time));
}
if ctx.kind == "staticlib" {
return artifact::archive_static(ctx, &objects, start_time);
}
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(
ctx.source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)
}
/// 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],
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
qt_include_dir: Option<PathBuf>,
) -> Result<Vec<String>, String> {
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
// Compile objects in parallel
common::parallel_build(
sources.len(),
|i| {
build_object(
compiler,
&sources[i],
&objects[i],
ctx,
qt_include_dir.as_deref(),
)
},
ctx.codegen_units,
)?;
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,
obj_path: &str,
ctx: &BuildContext,
qt_include_dir: Option<&Path>,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
// Skip if object is already up to date
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(compiler);
cmd.arg("-c");
if let Some(flag) = asm_lang_flag(source) {
cmd.arg("-x").arg(flag);
}
cmd.arg(source).arg("-o").arg(obj_path);
if ctx.kind == "sharedlib" {
cmd.arg("-fPIC");
}
if (ctx.freestanding || is_bare_metal_target(ctx.target) || is_flat_bin(ctx.kind))
&& ctx.language.to_lowercase() != "asm"
{
cmd.arg("-ffreestanding");
}
if ctx.panic_abort && ctx.language.to_lowercase() != "asm" {
if ctx.language.contains("++")
|| ctx.language.contains("cpp")
|| ctx.language.contains("cxx")
{
cmd.arg("-fno-exceptions");
}
cmd.arg("-fno-unwind-tables");
cmd.arg("-fno-asynchronous-unwind-tables");
}
if let Some(platform) = ctx.platform
&& !platform.trim().is_empty()
{
cmd.arg(format!("-march={}", platform));
}
if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" {
let ext = Path::new(source)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let is_cpp = matches!(ext, "cpp" | "cxx" | "cc");
if is_cpp && !ctx.cxx_standard.is_empty() {
cmd.arg(format!("-std={}", ctx.cxx_standard));
} else if !is_cpp {
cmd.arg(format!("-std={}", ctx.standard));
}
}
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);
}
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.include_dirs {
cmd.arg(format!("-I{dir}"));
}
if let Some(path) = qt_include_dir {
cmd.arg(format!("-I{}", path.display()));
}
let d_path = Path::new(obj_path).with_extension("d");
cmd.arg("-MMD").arg("-MF").arg(&d_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
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

@@ -0,0 +1,249 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod artifact;
pub mod cc_common;
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,
pub compiler: &'a str,
pub language: &'a str,
pub standard: &'a str,
pub cxx_standard: &'a str,
pub target: Option<&'a str>,
pub target_dir: Option<&'a str>,
pub kind: &'a str,
pub platform: Option<&'a str>,
pub linker: Option<&'a str>,
pub archiver: Option<&'a str>,
pub moc: Option<&'a str>,
pub uic: Option<&'a str>,
pub rcc: Option<&'a str>,
pub package_type: Option<&'a str>,
pub freestanding: bool,
pub panic_abort: bool,
pub codegen_units: usize,
pub source_roots: &'a [std::path::PathBuf],
pub exclude_dirs: &'a [std::path::PathBuf],
pub include_paths: &'a [String],
pub include_dirs: &'a [String],
pub lib_dirs: &'a [String],
pub libs: &'a [String],
pub cflags: &'a [String],
pub ldflags: &'a [String],
pub output_filename: Option<&'a str>,
pub output_extension: Option<&'a str>,
pub verbose: bool,
pub qt: bool,
}
/// Trait defining the build interface for different compilers and languages.
pub trait Builder {
#[allow(dead_code)]
fn id(&self) -> &'static str;
fn build(&self, ctx: &BuildContext) -> Result<f64, String>;
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 {
fn id(&self) -> &'static str {
"cc"
}
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
cc_common::build(ctx)
}
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
cc_common::collect_sources(ctx)
}
}
impl Builder for Msvc {
fn id(&self) -> &'static str {
"msvc"
}
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
msvc::build(ctx)
}
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
msvc::collect_sources(ctx)
}
}
impl Builder for Nasm {
fn id(&self) -> &'static str {
"nasm"
}
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
nasm::build(ctx)
}
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
nasm::collect_sources(ctx)
}
}
impl Builder for Gas {
fn id(&self) -> &'static str {
"gas"
}
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
gas::build(ctx)
}
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
gas::collect_sources(ctx)
}
}
impl Builder for Masm {
fn id(&self) -> &'static str {
"masm"
}
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
masm::build(ctx)
}
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
masm::collect_sources(ctx)
}
}
impl Builder for Fasm {
fn id(&self) -> &'static str {
"fasm"
}
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
fasm::build(ctx)
}
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
fasm::collect_sources(ctx)
}
}
impl Builder for Llc {
fn id(&self) -> &'static str {
"llvm_ir"
}
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
llvm_ir::build(ctx)
}
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
llvm_ir::collect_sources(ctx)
}
}
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;
}
if c == "as" || c.contains("gas") {
return &Gas;
}
if c.contains("llc") {
return &Llc;
}
if c.contains("fasm") {
return &Fasm;
}
if c.contains("nasm") {
return &Nasm;
}
if c == "ml" || c == "ml64" || c.contains("masm") {
return &Masm;
}
if c == "cl" || c.contains("msvc") {
return &Msvc;
}
&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!(
"Compiler not found: {}. Make sure it is installed and available in PATH.",
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"
} else {
match compiler.to_lowercase().as_str() {
"gas" | "gnu-as" => "as",
"nasm" => "nasm",
"fasm" | "fasm64" => "fasm",
"masm" | "ml" | "ml64" => "ml",
_ => compiler,
}
};
std::process::Command::new(name)
.arg("--version")
.output()
.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::*;
#[test]
fn builder_resolves_by_compiler_name() {
assert_eq!(builder_for("gcc").id(), "cc");
assert_eq!(builder_for("clang").id(), "cc");
assert_eq!(builder_for("clang++").id(), "cc");
assert_eq!(builder_for("").id(), "cc");
assert_eq!(builder_for("nasm").id(), "nasm");
assert_eq!(builder_for("as").id(), "gas");
assert_eq!(builder_for("llc-18").id(), "llvm_ir");
assert_eq!(builder_for("fasm").id(), "fasm");
assert_eq!(builder_for("ml64").id(), "masm");
assert_eq!(builder_for("cl").id(), "msvc");
assert_eq!(builder_for("clang-cl").id(), "msvc");
}
}

View File

@@ -15,14 +15,22 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::builder::BuildContext; /// MSVC build backend for DCR.
use crate::core::builder::common; ///
/// This module provides the MSVC-specific build implementation for C and C++
/// projects, including object compilation and linking for executables, static
/// libraries, shared libraries, and flat binaries.
use crate::core::build::builder::BuildContext;
use crate::core::build::builder::artifact;
use crate::core::build::common;
use crate::platform; use crate::platform;
use crate::utils::build::{is_compile_only, is_flat_bin};
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
use std::time::Instant; use std::time::Instant;
/// Builds the project target using the MSVC compiler.
pub fn build(ctx: &BuildContext) -> Result<f64, String> { pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let compiler = if ctx.compiler.is_empty() { let compiler = if ctx.compiler.is_empty() {
"cl" "cl"
@@ -33,6 +41,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if lang.contains("asm") { if lang.contains("asm") {
return Err("MSVC backend does not support build.language with asm".to_string()); return Err("MSVC backend does not support build.language with asm".to_string());
} }
if is_flat_bin(ctx.kind) && ctx.qt {
return Err("flat-bin is not supported with build.qt = true".to_string());
}
let start_time = Instant::now(); let start_time = Instant::now();
let sources = collect_sources(ctx)?; let sources = collect_sources(ctx)?;
let obj_dir = match ctx.target_dir { let obj_dir = match ctx.target_dir {
@@ -41,14 +52,27 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}; };
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "obj")?; let objects = build_objects(compiler, &sources, &obj_dir, ctx, "obj")?;
if is_compile_only(ctx.kind) && !is_flat_bin(ctx.kind) {
return Ok(common::elapsed_secs(start_time));
}
if is_flat_bin(ctx.kind) {
return link_flat_msvc(ctx, compiler, &objects, &obj_dir, start_time);
}
if ctx.kind == "staticlib" { if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir); let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
if !common::needs_link(&objects, &lib_path) { if !common::needs_link(&objects, &lib_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = common::elapsed_secs(start_time);
return Ok(elapsed); return Ok(elapsed);
} }
let mut cmd = Command::new(ctx.archiver.unwrap_or("lib")); 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}")); cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
} else {
cmd.arg("rcs").arg(&lib_path);
}
for obj in &objects { for obj in &objects {
cmd.arg(obj); cmd.arg(obj);
} }
@@ -57,7 +81,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
} }
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => { Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = common::elapsed_secs(start_time);
return Ok(elapsed); return Ok(elapsed);
} }
Ok(_) => return Err("Build failed".to_string()), Ok(_) => return Err("Build failed".to_string()),
@@ -123,17 +147,17 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}; };
if !common::needs_link(&objects, &out_path) { if !common::needs_link(&objects, &out_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = common::elapsed_secs(start_time);
return Ok(elapsed); return Ok(elapsed);
} }
cmd.arg(format!("/Fe:{out_path}")); cmd.arg("-o").arg(out_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() { if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd); eprintln!("[dcr] {:?}", cmd);
} }
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => { Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = common::elapsed_secs(start_time);
Ok(elapsed) Ok(elapsed)
} }
Ok(_) => Err("Build failed".to_string()), Ok(_) => Err("Build failed".to_string()),
@@ -141,6 +165,63 @@ 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,
objects: &[String],
obj_dir: &Path,
start_time: Instant,
) -> Result<f64, String> {
let out_path = artifact::flat_output_path(ctx);
if !common::needs_link(objects, &out_path) {
return Ok(common::elapsed_secs(start_time));
}
fs::create_dir_all(obj_dir).map_err(|e| format!("obj dir error: {e}"))?;
let intermediate = obj_dir
.join(format!("{}.flat.exe", ctx.project_name))
.to_string_lossy()
.to_string();
let mut cmd = Command::new(compiler);
cmd.arg("/nologo");
for obj in objects {
cmd.arg(obj);
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("/LIBPATH:{dir}"));
}
for lib in ctx.libs {
if lib.to_lowercase().ends_with(".lib") {
cmd.arg(lib);
} else {
cmd.arg(format!("{lib}.lib"));
}
}
cmd.arg(format!("/Fe:{intermediate}"));
if !ctx.ldflags.is_empty() {
cmd.arg("/link");
for flag in ctx.ldflags {
cmd.arg(flag);
}
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
let status = cmd
.status()
.map_err(|e| format!("flat-bin link failed: {e}"))?;
if !status.success() {
return Err("flat-bin link failed".to_string());
}
artifact::objcopy_binary(ctx, &intermediate, &out_path)?;
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> { pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
let extensions = source_extensions(ctx.language); let extensions = source_extensions(ctx.language);
common::collect_sources( common::collect_sources(
@@ -151,23 +232,12 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
) )
} }
fn source_extensions(language: &str) -> Vec<&str> { /// Source file extensions for `language` (owned `Vec` of static slices).
let mut out = Vec::new(); fn source_extensions(language: &str) -> Vec<&'static str> {
for part in language.split(',').map(|s| s.trim()) { crate::core::build::common::source_extensions(language)
let lang = part.to_lowercase();
match lang.as_str() {
"c" => out.extend(["c"]),
"c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]),
"asm" => {}
_ => {}
}
}
if out.is_empty() {
out.extend(["c"]);
}
out
} }
/// Maps the language and standard version to the corresponding MSVC flag string.
fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> { fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> {
let lang = language.to_lowercase(); let lang = language.to_lowercase();
let std = standard.to_lowercase(); let std = standard.to_lowercase();
@@ -191,6 +261,7 @@ fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String>
Err("Unsupported language".to_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> { fn msvc_arch_flag(platform: Option<&str>) -> Option<&'static str> {
let raw = platform?.trim(); let raw = platform?.trim();
if raw.is_empty() { if raw.is_empty() {
@@ -208,6 +279,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] { fn default_flags(profile: &str) -> &'static [&'static str] {
match profile { match profile {
"release" => &["/O2", "/DNDEBUG"], "release" => &["/O2", "/DNDEBUG"],
@@ -216,6 +288,7 @@ fn default_flags(profile: &str) -> &'static [&'static str] {
} }
} }
/// Compiles all sources into object files using parallel processing.
fn build_objects( fn build_objects(
compiler: &str, compiler: &str,
sources: &[String], sources: &[String],
@@ -228,13 +301,16 @@ fn build_objects(
.map(|s| common::object_path(obj_dir, s, obj_ext)) .map(|s| common::object_path(obj_dir, s, obj_ext))
.collect(); .collect();
common::parallel_build(sources.len(), |i| { common::parallel_build(
build_object(compiler, &sources[i], &objects[i], ctx) sources.len(),
})?; |i| build_object(compiler, &sources[i], &objects[i], ctx),
ctx.codegen_units,
)?;
Ok(objects) Ok(objects)
} }
/// Compiles a single source file to an object file.
fn build_object( fn build_object(
compiler: &str, compiler: &str,
source: &str, source: &str,
@@ -307,6 +383,7 @@ fn build_object(
let mut headers = Vec::new(); let mut headers = Vec::new();
let mut clean_stdout = String::new(); let mut clean_stdout = String::new();
// Parse compiler output to extract include file notes for dependency generation
for line in stdout.lines() { for line in stdout.lines() {
if let Some(stripped) = line.strip_prefix("Note: including file:") { if let Some(stripped) = line.strip_prefix("Note: including file:") {
headers.push(stripped.trim().to_string()); headers.push(stripped.trim().to_string());

284
src/core/build/cache.rs Normal file
View File

@@ -0,0 +1,284 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::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],
headers: &[PathBuf],
lib_files: &[PathBuf],
) -> Result<String, String> {
let mut hasher = Sha256::new();
hasher.update(ctx.profile.as_bytes());
hasher.update(ctx.project_name.as_bytes());
hasher.update(ctx.compiler.as_bytes());
hasher.update(ctx.language.as_bytes());
hasher.update(ctx.standard.as_bytes());
hasher.update(ctx.kind.as_bytes());
if let Some(v) = ctx.target_dir {
hasher.update(v.as_bytes());
}
if let Some(v) = ctx.platform {
hasher.update(v.as_bytes());
}
if let Some(v) = ctx.linker {
hasher.update(v.as_bytes());
}
if let Some(v) = ctx.archiver {
hasher.update(v.as_bytes());
}
for value in ctx.include_dirs {
hasher.update(value.as_bytes());
}
for value in ctx.lib_dirs {
hasher.update(value.as_bytes());
}
for value in ctx.libs {
hasher.update(value.as_bytes());
}
for value in ctx.cflags {
hasher.update(value.as_bytes());
}
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)?;
}
for header in headers {
update_hasher_with_file(&mut hasher, header)?;
}
for lib in lib_files {
update_hasher_with_file(&mut hasher, lib)?;
}
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() {
return false;
}
let cache_path = build_cache_path(ctx.profile, ctx.target_dir);
let cached = fs::read_to_string(cache_path).unwrap_or_default();
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() {
fs::create_dir_all(parent).map_err(|err| format!("Failed to create cache dir: {err}"))?;
}
fs::write(cache_path, format!("{fingerprint}\n"))
.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"),
None => Path::new("./target").join(profile).join(".dcr-build.hash"),
}
}
/// 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,
) -> Result<Vec<PathBuf>, String> {
let mut out = Vec::new();
let mut roots = Vec::new();
if ctx.source_roots.is_empty() {
roots.push(project_root.join("src"));
} else {
roots.extend(ctx.source_roots.iter().cloned());
}
let abs_root = project_root
.canonicalize()
.unwrap_or_else(|_| project_root.to_path_buf());
for dir in ctx.include_dirs {
let p = Path::new(dir).to_path_buf();
if let Ok(abs_p) = p.canonicalize() {
if abs_p.starts_with(&abs_root) {
roots.push(abs_p);
}
} else if p.starts_with(&abs_root) {
roots.push(p);
}
}
for root in roots {
if !root.exists() {
continue;
}
if root.is_file() {
if is_header_file(&root) {
out.push(root);
}
} else {
collect_header_files_rec(&root, &mut out, ctx.exclude_dirs, ctx.include_paths)?;
}
}
out.sort();
out.dedup();
Ok(out)
}
/// Recursively scans a directory to collect header files, respecting exclusion rules.
fn collect_header_files_rec(
dir: &Path,
out: &mut Vec<PathBuf>,
exclude_dirs: &[PathBuf],
include_paths: &[String],
) -> Result<(), String> {
if common::is_excluded(dir, exclude_dirs, include_paths) && include_paths.is_empty() {
return Ok(());
}
for entry in fs::read_dir(dir).map_err(|err| format!("read_dir error: {err}"))? {
let entry = entry.map_err(|err| format!("read_dir error: {err}"))?;
let path = entry.path();
if path.is_dir() {
if common::is_excluded(&path, exclude_dirs, include_paths) && include_paths.is_empty() {
continue;
}
collect_header_files_rec(&path, out, exclude_dirs, include_paths)?;
continue;
}
if !path.is_file() {
continue;
}
if common::is_excluded(&path, exclude_dirs, include_paths) {
continue;
}
if is_header_file(&path) {
out.push(path);
}
}
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 {
let dir_path = Path::new(dir);
for lib in ctx.libs {
for candidate in lib_candidates(lib) {
let path = dir_path.join(candidate);
if path.is_file() {
out.push(path);
}
}
}
}
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")];
}
if cfg!(target_os = "macos") {
return vec![
format!("lib{name}.a"),
format!("lib{name}.dylib"),
format!("lib{name}.so"),
];
}
vec![
format!("lib{name}.a"),
format!("lib{name}.so"),
format!("lib{name}.so.0"),
]
}
/// 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}"))?;
hasher.update(meta.len().to_le_bytes());
if let Ok(modified) = meta.modified()
&& let Ok(duration) = modified.duration_since(std::time::UNIX_EPOCH)
{
hasher.update(duration.as_nanos().to_le_bytes());
}
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

@@ -16,16 +16,87 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::fs; use std::fs;
use std::io::{self, IsTerminal, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, OnceLock}; use std::sync::{Mutex, OnceLock};
use std::time::Instant;
static OUTPUT_MUTEX: OnceLock<Mutex<()>> = OnceLock::new(); 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<()> { pub fn get_output_lock() -> &'static Mutex<()> {
OUTPUT_MUTEX.get_or_init(|| Mutex::new(())) 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;
if !was_dirty {
return;
}
if io::stderr().is_terminal() {
let _guard = get_output_lock().lock().unwrap();
let _ = writeln!(io::stderr());
let _ = io::stderr().flush();
}
}
/// 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;
}
if io::stderr().is_terminal() {
let _guard = get_output_lock().lock().unwrap();
let _ = writeln!(io::stderr());
let _ = io::stderr().flush();
}
}
/// 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 {
return;
};
if !io::stderr().is_terminal() {
return;
}
let _guard = get_output_lock().lock().unwrap();
let msg = format!("{label} [{done}/{total}]");
if PROGRESS_DIRTY.load(Ordering::SeqCst) {
eprint!("\r{msg:<80}");
} else {
eprint!("{msg}");
}
let _ = io::stderr().flush();
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( pub fn collect_sources(
roots: &[PathBuf], roots: &[PathBuf],
extensions: &[&str], extensions: &[&str],
@@ -34,12 +105,22 @@ pub fn collect_sources(
) -> Result<Vec<String>, String> { ) -> Result<Vec<String>, String> {
let mut sources = Vec::new(); let mut sources = Vec::new();
for root in roots { for root in roots {
if root.is_file() {
if let Some(ext) = root.extension().and_then(|e| e.to_str())
&& extensions.contains(&ext.to_lowercase().as_str())
{
sources.push(root.to_string_lossy().to_string());
}
} else {
collect_sources_rec(root, extensions, &mut sources, exclude_dirs, include_paths)?; collect_sources_rec(root, extensions, &mut sources, exclude_dirs, include_paths)?;
} }
}
sources.sort(); sources.sort();
Ok(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( fn collect_sources_rec(
dir: &Path, dir: &Path,
extensions: &[&str], extensions: &[&str],
@@ -57,9 +138,10 @@ fn collect_sources_rec(
if is_excluded(&full_dir, exclude_dirs, include_paths) && include_paths.is_empty() { if is_excluded(&full_dir, exclude_dirs, include_paths) && include_paths.is_empty() {
return Ok(()); return Ok(());
} }
let entries = fs::read_dir(&full_dir).map_err(|err| format!("src dir error: {err}"))?; let entries = fs::read_dir(&full_dir)
.map_err(|err| format!("src dir error for {:?}: {err}", full_dir))?;
for entry in entries { for entry in entries {
let entry = entry.map_err(|err| format!("src dir error: {err}"))?; let entry = entry.map_err(|err| format!("src dir error for {:?}: {err}", full_dir))?;
let path = entry.path(); let path = entry.path();
if path.is_dir() { if path.is_dir() {
if is_excluded(&path, exclude_dirs, include_paths) && include_paths.is_empty() { if is_excluded(&path, exclude_dirs, include_paths) && include_paths.is_empty() {
@@ -86,6 +168,8 @@ fn collect_sources_rec(
Ok(()) 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 { pub fn is_excluded(path: &Path, exclude_dirs: &[PathBuf], include_paths: &[String]) -> bool {
if matches_patterns(path, include_paths, true) { if matches_patterns(path, include_paths, true) {
return false; return false;
@@ -93,9 +177,33 @@ pub fn is_excluded(path: &Path, exclude_dirs: &[PathBuf], include_paths: &[Strin
if matches_patterns(path, include_paths, false) { if matches_patterns(path, include_paths, false) {
return true; 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 { fn matches_patterns(path: &Path, patterns: &[String], positive: bool) -> bool {
if patterns.is_empty() { if patterns.is_empty() {
return false; return false;
@@ -160,21 +268,72 @@ fn matches_patterns(path: &Path, patterns: &[String], positive: bool) -> bool {
false false
} }
/// Returns true if the string contains any glob magic characters (*, ?, or [).
pub fn has_glob_magic(value: &str) -> bool { pub fn has_glob_magic(value: &str) -> bool {
value.chars().any(|c| matches!(c, '*' | '?' | '[')) value.chars().any(|c| matches!(c, '*' | '?' | '['))
} }
pub fn parallel_build<F>(total_tasks: usize, task_fn: F) -> Result<(), String> /// 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 {
"S" => Some("assembler-with-cpp"),
"s" | "asm" => Some("assembler"),
_ => None,
}
}
/// 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()) {
if let Some(lang) = crate::core::build::language::language_for_token(part) {
out.extend(lang.extensions().iter().copied());
}
}
if out.is_empty() {
out.extend(["c"]);
}
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 where
F: Fn(usize) -> Result<(), String> + Sync + Send, F: Fn(usize) -> Result<(), String> + Sync + Send,
{ {
let num_threads = std::thread::available_parallelism() let available = std::thread::available_parallelism()
.map(|n| n.get()) .map(|n| n.get())
.unwrap_or(1); .unwrap_or(1);
let num_threads = if max_jobs > 0 {
max_jobs.min(available)
} else {
available
};
let counter = std::sync::atomic::AtomicUsize::new(0); let counter = std::sync::atomic::AtomicUsize::new(0);
let completed = std::sync::atomic::AtomicUsize::new(0);
let err_msg = std::sync::Mutex::new(None); let err_msg = std::sync::Mutex::new(None);
if total_tasks > 0 {
report_compile_progress(0, total_tasks);
}
std::thread::scope(|s| { std::thread::scope(|s| {
for _ in 0..num_threads { for _ in 0..num_threads {
s.spawn(|| { s.spawn(|| {
@@ -195,6 +354,9 @@ where
} }
break; break;
} }
let done = completed.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
report_compile_progress(done, total_tasks);
} }
}); });
} }
@@ -207,11 +369,24 @@ where
Ok(()) 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> { pub fn run_command_sync_output(cmd: &mut Command) -> Result<(), String> {
let output = cmd.output().map_err(|err| format!("Build failed: {err}"))?; let program = cmd.get_program().to_string_lossy().into_owned();
let _guard = OUTPUT_MUTEX.get_or_init(|| Mutex::new(())).lock().unwrap(); let output = cmd.output().map_err(|err| {
if err.kind() == std::io::ErrorKind::NotFound {
format!("{program} not found (check PATH / [toolchain])")
} else {
format!("Build failed: {err}")
}
})?;
let has_out = !output.stdout.is_empty() || !output.stderr.is_empty();
if has_out {
interrupt_progress_for_output();
}
let _guard = get_output_lock().lock().unwrap();
if !output.stdout.is_empty() { if !output.stdout.is_empty() {
print!("{}", String::from_utf8_lossy(&output.stdout)); eprint!("{}", String::from_utf8_lossy(&output.stdout));
} }
if !output.stderr.is_empty() { if !output.stderr.is_empty() {
eprint!("{}", String::from_utf8_lossy(&output.stderr)); eprint!("{}", String::from_utf8_lossy(&output.stderr));
@@ -223,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 { pub fn normalize_source_path(path: &Path) -> String {
if !path.is_absolute() { if !path.is_absolute() {
return path.to_string_lossy().to_string(); return path.to_string_lossy().to_string();
@@ -235,6 +412,8 @@ pub fn normalize_source_path(path: &Path) -> String {
path.to_string_lossy().to_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 { pub fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
let src_path = Path::new(source); let src_path = Path::new(source);
let stripped = src_path.strip_prefix("./").unwrap_or(src_path); let stripped = src_path.strip_prefix("./").unwrap_or(src_path);
@@ -264,9 +443,19 @@ pub fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
} else { } else {
out.set_extension(obj_ext); 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 { pub fn needs_rebuild(source: &str, object: &str) -> bool {
let src_time = fs::metadata(source).and_then(|m| m.modified()); let src_time = fs::metadata(source).and_then(|m| m.modified());
let obj_time = fs::metadata(object).and_then(|m| m.modified()); let obj_time = fs::metadata(object).and_then(|m| m.modified());
@@ -295,13 +484,21 @@ pub fn needs_rebuild(source: &str, object: &str) -> bool {
return true; return true;
} }
} else { } else {
return true; // Missing dependency triggers rebuild return true;
} }
} }
} }
false 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 { pub fn needs_link(objects: &[String], output_path: &str) -> bool {
let out_meta = match fs::metadata(output_path) { let out_meta = match fs::metadata(output_path) {
Ok(m) => m, Ok(m) => m,
@@ -327,6 +524,8 @@ pub fn needs_link(objects: &[String], output_path: &str) -> bool {
false 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> { fn parse_d_file(content: &str) -> Vec<String> {
let mut deps = Vec::new(); let mut deps = Vec::new();
let mut target_end = 0; let mut target_end = 0;
@@ -367,7 +566,11 @@ fn parse_d_file(content: &str) -> Vec<String> {
} }
in_escape = false; in_escape = false;
} else if c == '\\' { } 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() { } else if c.is_whitespace() {
if !current_path.is_empty() { if !current_path.is_empty() {
deps.push(current_path.clone()); deps.push(current_path.clone());
@@ -394,12 +597,18 @@ mod tests {
static TEST_COUNTER: AtomicUsize = AtomicUsize::new(0); static TEST_COUNTER: AtomicUsize = AtomicUsize::new(0);
static CWD_LOCK: Mutex<()> = Mutex::new(()); 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 { fn temp_dir(prefix: &str) -> PathBuf {
let n = TEST_COUNTER.fetch_add(1, Ordering::SeqCst); let n = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let dir = std::env::temp_dir().join(format!("dcr_test_{prefix}_{n}")); let dir = std::env::temp_dir().join(format!("dcr_test_{prefix}_{n}"));
let _ = fs::remove_dir_all(&dir); let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap(); fs::create_dir_all(&dir).unwrap();
dir fs::canonicalize(&dir).unwrap_or(dir)
} }
fn default_roots() -> Vec<PathBuf> { fn default_roots() -> Vec<PathBuf> {
@@ -501,7 +710,6 @@ mod tests {
let src = dir.join("test.c"); let src = dir.join("test.c");
let obj = dir.join("test.o"); let obj = dir.join("test.o");
fs::write(&src, "int main() {}").unwrap(); fs::write(&src, "int main() {}").unwrap();
// Sleep briefly to ensure mtime difference
std::thread::sleep(std::time::Duration::from_millis(50)); std::thread::sleep(std::time::Duration::from_millis(50));
fs::write(&obj, "").unwrap(); fs::write(&obj, "").unwrap();
assert!(!needs_rebuild( assert!(!needs_rebuild(
@@ -565,7 +773,7 @@ mod tests {
fs::write(src.join("utils.c"), "").unwrap(); fs::write(src.join("utils.c"), "").unwrap();
fs::write(src.join("README.md"), "").unwrap(); // should be ignored 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(); let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap(); std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new(); let include: Vec<String> = Vec::new();
@@ -589,7 +797,7 @@ mod tests {
fs::write(src.join("other.cc"), "").unwrap(); fs::write(src.join("other.cc"), "").unwrap();
fs::write(src.join("skip.c"), "").unwrap(); // should be ignored 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(); let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap(); std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new(); let include: Vec<String> = Vec::new();
@@ -607,7 +815,7 @@ mod tests {
let src = dir.join("src"); let src = dir.join("src");
fs::create_dir_all(&src).unwrap(); fs::create_dir_all(&src).unwrap();
let _guard = CWD_LOCK.lock().unwrap(); let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap(); let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap(); std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new(); let include: Vec<String> = Vec::new();
@@ -629,7 +837,7 @@ mod tests {
fs::write(vendor.join("lib.c"), "").unwrap(); // should be excluded fs::write(vendor.join("lib.c"), "").unwrap(); // should be excluded
let exclude = vec![vendor.clone()]; let exclude = vec![vendor.clone()];
let _guard = CWD_LOCK.lock().unwrap(); let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap(); let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap(); std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new(); let include: Vec<String> = Vec::new();
@@ -651,7 +859,7 @@ mod tests {
fs::write(src.join("main.c"), "").unwrap(); fs::write(src.join("main.c"), "").unwrap();
fs::write(sub.join("nested.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(); let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap(); std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new(); let include: Vec<String> = Vec::new();
@@ -675,7 +883,7 @@ mod tests {
let exclude = vec![boot.clone()]; let exclude = vec![boot.clone()];
let include = vec!["src/boot/arch/**".to_string()]; 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(); let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap(); std::env::set_current_dir(&dir).unwrap();
let roots = default_roots(); let roots = default_roots();
@@ -702,7 +910,7 @@ mod tests {
"!src/legacy/**".to_string(), "!src/legacy/**".to_string(),
"src/legacy/allow/**".to_string(), "src/legacy/allow/**".to_string(),
]; ];
let _guard = CWD_LOCK.lock().unwrap(); let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap(); let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap(); std::env::set_current_dir(&dir).unwrap();
let roots = default_roots(); let roots = default_roots();
@@ -725,7 +933,7 @@ mod tests {
let exclude: Vec<PathBuf> = Vec::new(); let exclude: Vec<PathBuf> = Vec::new();
let include = vec!["!src/gen/**".to_string()]; let include = vec!["!src/gen/**".to_string()];
let _guard = CWD_LOCK.lock().unwrap(); let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap(); let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap(); std::env::set_current_dir(&dir).unwrap();
let roots = default_roots(); let roots = default_roots();
@@ -760,35 +968,83 @@ mod tests {
#[test] #[test]
fn needs_rebuild_header_modified() { fn needs_rebuild_header_modified() {
let dir = temp_dir("rebuild_header"); let dir = temp_dir("rebuild_header");
let src = dir.join("test.c"); fs::write(dir.join("test.c"), "int main() {}").unwrap();
let header = dir.join("test.h"); fs::write(dir.join("test.h"), "#define A 1").unwrap();
let obj = dir.join("test.o"); fs::write(dir.join("test.o"), "").unwrap();
let d_file = dir.join("test.d");
fs::write(&src, "int main() {}").unwrap(); let _guard = cwd_lock();
fs::write(&header, "#define A 1").unwrap(); let prev = std::env::current_dir().unwrap();
std::thread::sleep(std::time::Duration::from_millis(50)); std::env::set_current_dir(&dir).unwrap();
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();
fs::write("test.d", "test.o: test.c missing.h").unwrap();
assert!( assert!(
!needs_rebuild(&src.to_string_lossy(), &obj.to_string_lossy()), needs_rebuild("test.c", "test.o"),
"should be fresh" "missing dep in .d must force rebuild"
); );
std::thread::sleep(std::time::Duration::from_millis(50)); fs::write("test.h", "#define A 1").unwrap();
fs::write(&header, "#define A 2").unwrap(); // modify header 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!( 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" "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]
fn asm_lang_flag_returns_correct_flag() {
assert_eq!(asm_lang_flag("foo.S"), Some("assembler-with-cpp"));
assert_eq!(asm_lang_flag("bar.s"), Some("assembler"));
assert_eq!(asm_lang_flag("baz.asm"), Some("assembler"));
assert_eq!(asm_lang_flag("main.c"), None);
}
#[test]
fn source_extensions_parses_language_string() {
let c = source_extensions("c");
assert!(c.contains(&"c"));
assert!(!c.contains(&"cpp"));
let cpp = source_extensions("c++");
assert!(cpp.contains(&"cpp"));
assert!(cpp.contains(&"cxx"));
assert!(cpp.contains(&"cc"));
let asm = source_extensions("asm");
assert!(asm.contains(&"s"));
assert!(asm.contains(&"S"));
assert!(asm.contains(&"asm"));
let mixed = source_extensions("c, c++");
assert!(mixed.contains(&"c"));
assert!(mixed.contains(&"cpp"));
let unknown = source_extensions("rust");
assert_eq!(unknown, vec!["c"]);
}
#[test]
fn elapsed_secs_returns_positive() {
let start = std::time::Instant::now();
std::thread::sleep(std::time::Duration::from_millis(50));
let elapsed = elapsed_secs(start);
assert!(elapsed > 0.0);
} }
} }

1276
src/core/build/engine.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,207 @@
use crate::core::build::builder::BuildContext;
use crate::core::build::builder::artifact;
use crate::core::build::common;
use crate::utils::build::{is_compile_only, is_flat_bin};
use std::path::Path;
use std::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()
.filter(|f| {
let f = f.as_str();
f.starts_with("-I") || f.starts_with("-i") || f == "-g"
})
.cloned()
.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,
default_assembler: &str,
extensions: &[&str],
build_object: F,
) -> Result<f64, String>
where
F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync,
{
build_assembly_with_mode(
ctx,
backend_name,
default_assembler,
extensions,
FlatEmit::Direct,
build_object,
)
}
/// 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,
default_assembler: &str,
extensions: &[&str],
build_object: F,
) -> Result<f64, String>
where
F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync,
{
build_assembly_with_mode(
ctx,
backend_name,
default_assembler,
extensions,
FlatEmit::ViaObjcopy,
build_object,
)
}
/// 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,
default_assembler: &str,
extensions: &[&str],
flat_mode: FlatEmit,
build_object: F,
) -> Result<f64, String>
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"
}) {
return Err(format!(
"{backend_name} backend requires build.language = \"asm\" (or llvm_ir for LLC)"
));
}
let assembler = if ctx.compiler.is_empty() {
default_assembler
} else {
ctx.compiler
};
let start_time = Instant::now();
let sources = common::collect_sources(
ctx.source_roots,
extensions,
ctx.exclude_dirs,
ctx.include_paths,
)?;
let flat = is_flat_bin(ctx.kind);
let obj_dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
let objects = match (flat, flat_mode) {
// Dispatch based on flat binary kind and emission mode
(true, FlatEmit::Direct) => {
let outs: Vec<String> = sources
.iter()
.map(|s| artifact::flat_source_output_path(ctx, s))
.collect();
common::parallel_build(
sources.len(),
|i| build_object(assembler, &sources[i], &outs[i], ctx),
ctx.codegen_units,
)?;
outs
}
(true, FlatEmit::ViaObjcopy) => {
let intermediates: Vec<String> = sources
.iter()
.map(|s| common::object_path(&obj_dir, s, "o"))
.collect();
common::parallel_build(
sources.len(),
|i| build_object(assembler, &sources[i], &intermediates[i], ctx),
ctx.codegen_units,
)?;
let mut outs = Vec::with_capacity(sources.len());
for (i, src) in sources.iter().enumerate() {
let out = artifact::flat_source_output_path(ctx, src);
artifact::objcopy_binary(ctx, &intermediates[i], &out)?;
outs.push(out);
}
outs
}
(false, _) => build_objects_common(assembler, &sources, &obj_dir, ctx, &build_object)?,
};
if is_compile_only(ctx.kind) {
return Ok(common::elapsed_secs(start_time));
}
if ctx.kind == "staticlib" {
return artifact::archive_static(ctx, &objects, start_time);
}
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],
obj_dir: &Path,
ctx: &BuildContext,
build_object: &F,
) -> Result<Vec<String>, String>
where
F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync,
{
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, "o"))
.collect();
common::parallel_build(
sources.len(),
|i| build_object(assembler, &sources[i], &objects[i], ctx),
ctx.codegen_units,
)?;
Ok(objects)
}

View File

@@ -0,0 +1,53 @@
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;
/// 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,
&["asm", "fasm"],
ctx.exclude_dirs,
ctx.include_paths,
)
}
/// Assembles one FASM source into an object file.
fn build_object(
assembler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
// Skip rebuilding if the object is up to date
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(assembler);
cmd.arg(source).arg(obj_path);
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
cmd.arg(flag);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
common::run_command_sync_output(&mut cmd)
}

View File

@@ -0,0 +1,86 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::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;
/// 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")
.arg("as")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "as".to_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,
&["s"],
ctx.exclude_dirs,
ctx.include_paths,
)
}
/// 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,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
// 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);
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
cmd.arg(flag);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
common::run_command_sync_output(&mut cmd)
}

View File

@@ -0,0 +1,65 @@
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;
/// 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,
&["asm"],
ctx.exclude_dirs,
ctx.include_paths,
)
}
/// 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,
obj_path: &str,
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(());
}
let mut cmd = Command::new(assembler);
cmd.arg("/nologo")
.arg("/c")
.arg("/Fo")
.arg(obj_path)
.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);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
common::run_command_sync_output(&mut cmd)
}

View File

@@ -0,0 +1,45 @@
// 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/>.
/// Assembly language module for the DCR build system.
pub mod common;
pub mod fasm;
pub mod gas;
pub mod masm;
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

@@ -0,0 +1,128 @@
// 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/>.
/// Support for NASM assembly language in the DCR build system.
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::language::asm::common as asm;
use std::fs;
use std::path::Path;
use std::process::Command;
/// 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(
ctx.source_roots,
&["asm"],
ctx.exclude_dirs,
ctx.include_paths,
)
}
/// 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,
obj_path: &str,
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 {
nasm_format(ctx.platform)
};
let mut cmd = Command::new(assembler);
cmd.arg("-f")
.arg(format)
.arg(source)
.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);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
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('-', "_");
if p == "x86" || (p.starts_with('i') && p.ends_with("86") && p.len() == 4) {
#[cfg(target_os = "macos")]
{
return "macho32";
}
#[cfg(target_os = "windows")]
{
return "win32";
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
return "elf32";
}
}
}
#[cfg(target_os = "linux")]
{
"elf64"
}
#[cfg(target_os = "macos")]
{
"macho64"
}
#[cfg(target_os = "windows")]
{
"win64"
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
"elf64"
}
}

View File

@@ -0,0 +1,38 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::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

@@ -0,0 +1,40 @@
// 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/>.
/// 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

@@ -0,0 +1,149 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::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}"))?;
let extensions = vec!["ui", "qrc", "h", "hpp", "hxx"];
let files = common::collect_sources(
ctx.source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)?;
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() {
if flag.starts_with("-I") || flag.starts_with("-D") {
qt_moc_args.push(flag.to_string());
}
}
}
}
let mut generated_any = false;
for file in files {
let path = Path::new(&file);
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
match ext {
"ui" => {
if let Some(uic) = ctx.uic {
let out_name = path.file_stem().unwrap().to_str().unwrap();
let out_file = qt_dir.join(format!("ui_{out_name}.h"));
run_command(
uic,
&[path.to_str().unwrap(), "-o", out_file.to_str().unwrap()],
ctx.verbose,
)?;
generated_any = true;
}
}
"qrc" => {
if let Some(rcc) = ctx.rcc {
let out_name = path.file_stem().unwrap().to_str().unwrap();
let out_file = qt_dir.join(format!("qrc_{out_name}.cpp"));
run_command(
rcc,
&[
path.to_str().unwrap(),
"-name",
out_name,
"-o",
out_file.to_str().unwrap(),
],
ctx.verbose,
)?;
generated_any = true;
}
}
"h" | "hpp" | "hxx" => {
if let Some(moc) = ctx.moc {
let content = fs::read_to_string(path)
.map_err(|e| format!("Failed to read {}: {e}", path.display()))?;
if content.contains("Q_OBJECT") {
let out_name = path.file_stem().unwrap().to_str().unwrap();
let out_file = qt_dir.join(format!("moc_{out_name}.cpp"));
let mut moc_args = Vec::new();
for arg in &qt_moc_args {
moc_args.push(arg.as_str());
}
for inc in ctx.include_paths.iter().chain(ctx.include_dirs.iter()) {
moc_args.push("-I");
moc_args.push(inc);
}
moc_args.push(path.to_str().unwrap());
moc_args.push("-o");
moc_args.push(out_file.to_str().unwrap());
run_command(moc, &moc_args, ctx.verbose)?;
generated_any = true;
}
}
}
_ => {}
}
}
if generated_any {
Ok(Some(qt_dir))
} else {
Ok(None)
}
}
/// 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);
if verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(s) if s.success() => Ok(()),
Ok(s) => Err(format!("Command {} failed with status {}", cmd_path, s)),
Err(e) => Err(format!("Failed to execute {}: {}", cmd_path, e)),
}
}

View File

@@ -0,0 +1,68 @@
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;
/// 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,
&["ll"],
ctx.exclude_dirs,
ctx.include_paths,
)
}
/// Builds a single LLVM IR source file into an object file.
fn build_object(
assembler: &str,
source: &str,
obj_path: &str,
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}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(assembler);
cmd.arg("-filetype=obj").arg(source).arg("-o").arg(obj_path);
for flag in ctx.cflags {
cmd.arg(flag);
}
common::run_command_sync_output(&mut cmd)
}

View File

@@ -0,0 +1,67 @@
// 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/>.
/// 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))
}
#[cfg(test)]
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");
assert_eq!(language_for_token("c++").unwrap().id(), "cxx");
assert_eq!(language_for_token("cpp").unwrap().id(), "cxx");
assert_eq!(language_for_token("cxx").unwrap().id(), "cxx");
assert_eq!(language_for_token("asm").unwrap().id(), "asm");
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"]);
assert_eq!(cxx::Cxx.extensions(), &["cpp", "cxx", "cc"]);
assert_eq!(asm::Asm.extensions(), &["s", "S", "asm"]);
}
}

View File

@@ -15,19 +15,20 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::path::Path; /// 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;
pub mod cache;
pub mod common;
pub mod engine;
pub mod language;
pub mod report;
pub mod resolve;
pub mod steps;
#[allow(dead_code)] pub use engine::run_build;
pub fn fetch_git_dep( pub use report::{BuildEvent, BuildReporter, BuildRequest};
url: &str, pub use resolve::get_build_string_with_profile;
target_dir: &Path,
branch: Option<&str>,
tag: Option<&str>,
rev: Option<&str>,
) -> Result<(), String> {
if target_dir.exists() {
return Ok(());
}
crate::utils::git::git_clone_and_checkout(url, target_dir, branch, tag, rev)
}

93
src/core/build/report.rs Normal file
View File

@@ -0,0 +1,93 @@
// 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/>.
// The reporter decouples the build engine from any particular frontend: the CLI
// renders events as coloured lines, a TUI/GUI would push them into a widget and
// MCP would serialise them to JSON. The engine only ever calls `on_event`.
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>,
pub force: bool,
pub verbose: bool,
pub workspace: Option<String>,
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,
total: usize,
target: &'a str,
},
ProjectStart {
name: &'a str,
profile: &'a str,
target: &'a str,
},
DepBuilding {
name: &'a str,
version: &'a str,
},
DepReady {
name: &'a str,
version: &'a str,
rebuilt: bool,
},
Compiling {
name: &'a str,
version: &'a str,
},
Packing {
path: &'a str,
},
CompilerOutput {
stream: &'a str,
text: &'a str,
},
Finished {
secs: f64,
},
}
/// 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<'_>);
}

267
src/core/build/resolve.rs Normal file
View File

@@ -0,0 +1,267 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build_config::Config;
use crate::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,
field: &str,
profile: &str,
target: Option<&str>,
) -> Option<toml::Value> {
// Order: target.profile, profile.target, target, profile, base
let keys = [
target.map(|t| {
format!(
"{}.{}.{}.{}",
section,
normalize_target_os(t),
profile,
field
)
}),
target.map(|t| {
format!(
"{}.{}.{}.{}",
section,
profile,
normalize_target_os(t),
field
)
}),
target.map(|t| format!("{}.{}.{}", section, normalize_target_os(t), field)),
Some(format!("{}.{}.{}", section, profile, field)),
Some(format!("{}.{}", section, field)),
];
for key in keys.into_iter().flatten() {
if let Some(val) = config.get(&key) {
return Some(val.clone());
}
}
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,
profile: &str,
target: Option<&str>,
) -> bool {
get_config_value_raw(config, section, "inherit", profile, target)
.and_then(|v| v.as_bool())
.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,
field: &str,
profile: &str,
target: Option<&str>,
) -> Option<String> {
get_config_value_raw(config, section, field, profile, target)
.and_then(|v| v.as_str().map(|s| s.trim().to_string()))
.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,
profile: &str,
target: Option<&str>,
) -> String {
if get_inherit(config, "build", profile, target) {
get_config_value(config, "build", field, profile, target)
.unwrap_or_else(|| get_config_str(config, &format!("build.{field}")))
} else {
get_config_value(config, "build", field, profile, target).unwrap_or_default()
}
}
/// 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,
field: &str,
profile: &str,
target: Option<&str>,
) -> Option<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,
field: &str,
profile: &str,
target: Option<&str>,
) -> Result<Vec<String>, String> {
match get_config_value_raw(config, &format!("build.{lang}"), field, profile, target) {
Some(val) => parse_string_array(&val, field),
None => Ok(Vec::new()),
}
}
/// 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()
.ok_or_else(|| format!("{key} must be an array of strings"))?;
let mut out = Vec::new();
for item in arr {
let s = item
.as_str()
.ok_or_else(|| format!("{key} must be an array of strings"))?;
out.push(s.to_string());
}
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,
profile: &str,
target: Option<&str>,
) -> Result<Vec<String>, String> {
let inherit = get_inherit(config, "build", profile, target);
let mut out = if inherit {
get_config_list(config, &format!("build.{field}"))?
} else {
Vec::new()
};
if let Some(val) = get_config_value_raw(config, "build", field, profile, target) {
if let Some(_arr) = val.as_array() {
let is_base = inherit
&& config
.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 {
out.extend(custom);
} else {
out = custom;
}
}
}
} else if inherit {
if let Some(table) = profile_table(config, profile)
&& let Some(value) = table.get(field)
{
let mut extra = parse_string_array(value, &format!("build.{profile}.{field}"))?;
out.append(&mut extra);
}
if let Some(t) = target {
let normalized_t = normalize_target_os(t);
if let Some(table) = profile_table(config, normalized_t)
&& let Some(value) = table.get(field)
{
let mut extra =
parse_string_array(value, &format!("build.{normalized_t}.{field}"))?;
out.append(&mut extra);
}
}
}
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,
profile: &str,
) -> Result<Vec<String>, String> {
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,
None => return Ok(Vec::new()),
};
let arr = value
.as_array()
.ok_or_else(|| format!("{key} must be an array of strings"))?;
let mut out = Vec::new();
for item in arr {
let s = item
.as_str()
.ok_or_else(|| format!("{key} must be an array of strings"))?;
out.push(s.to_string());
}
Ok(out)
}
#[cfg(test)]
mod tests {
use crate::utils::build::normalize_target_os;
#[test]
fn test_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");
assert_eq!(
normalize_target_os("x86_64-unknown-linux-gnu"),
"x86_64-unknown-linux-gnu"
);
assert_eq!(normalize_target_os("unknown"), "unknown");
}
}

417
src/core/build/steps.rs Normal file
View File

@@ -0,0 +1,417 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::engine::ToolchainExecs;
use crate::core::build_config::Config;
use crate::utils::build::{VersionInfo, profile_table, substitute_vars};
use glob::glob;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Clone)]
pub(crate) struct BuildStep {
name: String,
input: String,
output: String,
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,
pub(crate) version_major: &'a str,
pub(crate) version_minor: &'a str,
pub(crate) version_patch: &'a str,
pub(crate) version_suffix: &'a str,
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()
.ok_or_else(|| format!("{key} must be an array"))?;
let mut out = Vec::new();
for item in arr {
let tbl = item
.as_table()
.ok_or_else(|| format!("{key} entries must be tables"))?;
let name = tbl
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
let input = tbl
.get("in")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
let output = tbl
.get("out")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
let cmd = tbl
.get("cmd")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
if name.is_empty() || input.is_empty() || output.is_empty() || cmd.is_empty() {
return Err(format!("{key} entries must include name, in, out, cmd"));
}
out.push(BuildStep {
name,
input,
output,
cmd,
});
}
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,
profile: &str,
) -> Result<Vec<BuildStep>, String> {
if let Some(table) = profile_table(config, profile)
&& let Some(value) = table.get(field)
{
return get_build_steps_from_value(value, &format!("build.{profile}.{field}"));
}
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,
None => return Ok(Vec::new()),
};
get_build_steps_from_value(value, key)
}
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,
step_flags: &str,
vars: &StepVars,
cancel: &Arc<AtomicBool>,
rep: &mut dyn BuildReporter,
) -> Result<(), String> {
for step in steps {
if cancel.load(std::sync::atomic::Ordering::SeqCst) {
return Err("Build interrupted".to_string());
}
run_build_step(step, tools, step_flags, vars, cancel, rep)?;
}
Ok(())
}
/// Executes a single build step, handling glob expansion, output paths, and command substitution.
fn run_build_step(
step: &BuildStep,
tools: &ToolchainExecs,
step_flags: &str,
vars: &StepVars,
cancel: &Arc<AtomicBool>,
rep: &mut dyn BuildReporter,
) -> 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(());
}
let needs_stem = step.output.contains("{stem}");
if inputs.len() > 1 && !needs_stem {
return Err(format!(
"build.steps '{}' output must include {{stem}} for multiple inputs",
step.name
));
}
for input in inputs {
if !input.is_file() {
continue;
}
let stem = input.file_stem().and_then(|v| v.to_str()).unwrap_or("");
let out_path = PathBuf::from(expand_step_value(&step.output, stem, vars));
if !should_run_step(&input, &out_path) {
continue;
}
if let Some(parent) = out_path.parent() {
fs::create_dir_all(parent)
.map_err(|err| format!("Failed to create step output dir: {err}"))?;
}
let cmd = substitute_step_cmd(&step.cmd, &input, &out_path, tools, step_flags, stem, vars);
let status = run_shell_command(&cmd, cancel, rep)
.map_err(|err| format!("Failed to run step '{}': {err}", step.name))?;
if !status.success() {
return Err(format!("Step '{}' failed", step.name));
}
}
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)? {
if path.is_file() {
let _ = fs::remove_file(&path);
}
}
}
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}"))?;
for entry in entries {
let path = entry.map_err(|err| format!("glob error: {err}"))?;
out.push(path);
}
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);
let matches = expand_glob(&expanded)?;
if matches.is_empty() {
return Err(format!("Expected artifact not found: {expanded}"));
}
}
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());
match (in_time, out_time) {
(Ok(i), Ok(o)) => i > o,
(Ok(_), Err(_)) => true,
_ => true,
}
}
/// Substitutes variables into step command template and replaces placeholders.
fn substitute_step_cmd(
template: &str,
input: &Path,
output: &Path,
tools: &ToolchainExecs,
step_flags: &str,
stem: &str,
vars: &StepVars,
) -> String {
let info = make_version_info(vars);
let s = substitute_vars(template, &info, vars.profile, "");
s.replace("{in}", &input.to_string_lossy())
.replace("{out}", &output.to_string_lossy())
.replace("{uic}", &tools.uic)
.replace("{moc}", &tools.moc)
.replace("{rcc}", &tools.rcc)
.replace("{cflags}", step_flags)
.replace("{stem}", stem)
}
/// Builds a list of compiler flags from cflags, include dirs, and compiler type.
pub(crate) fn build_step_flags(
cflags: &[String],
include_dirs: &[String],
compiler: &str,
) -> String {
let mut out = Vec::new();
let msvc_style = is_msvc_compiler(compiler) || cflags.iter().any(|f| f.starts_with('/'));
for flag in cflags {
if flag.starts_with("-I") || flag.starts_with("-D") {
out.push(flag.clone());
}
if flag.starts_with("/I") || flag.starts_with("/D") {
out.push(flag.clone());
}
if msvc_style && flag.starts_with("-D") {
out.push(format!("/D{}", flag.trim_start_matches("-D")));
}
}
for dir in include_dirs {
out.push(format!("-I{dir}"));
if msvc_style {
out.push(format!("/I{dir}"));
}
}
out.sort();
out.dedup();
out.into_iter()
.map(quote_step_arg)
.collect::<Vec<_>>()
.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;
}
let escaped = arg.replace('"', "\\\"");
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")
|| lower == "cl"
|| lower.contains("clang-cl")
|| lower.contains("msvc")
}
/// Runs shell command with output capture and cancellation support.
fn run_shell_command(
cmd: &str,
cancel: &Arc<AtomicBool>,
rep: &mut dyn BuildReporter,
) -> Result<std::process::ExitStatus, std::io::Error> {
let mut child = if cfg!(target_os = "windows") {
std::process::Command::new("cmd")
.arg("/C")
.arg(cmd)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
} else {
std::process::Command::new("sh")
.arg("-c")
.arg(cmd)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
}?;
let stdout = child.stdout.take().unwrap();
let stderr = child.stderr.take().unwrap();
use std::io::{BufRead, BufReader};
use std::sync::mpsc;
let (tx, rx) = mpsc::channel();
let cancel_clone = cancel.clone();
let tx_stdout = tx.clone();
let stdout_thread = std::thread::spawn(move || {
let reader = BufReader::new(stdout);
for line in reader.lines() {
if cancel_clone.load(std::sync::atomic::Ordering::SeqCst) {
break;
}
if let Ok(line) = line {
let _ = tx_stdout.send(("stdout", line));
}
}
});
let cancel_clone = cancel.clone();
let tx_stderr = tx.clone();
let stderr_thread = std::thread::spawn(move || {
let reader = BufReader::new(stderr);
for line in reader.lines() {
if cancel_clone.load(std::sync::atomic::Ordering::SeqCst) {
break;
}
if let Ok(line) = line {
let _ = tx_stderr.send(("stderr", line));
}
}
});
let status = child.wait()?;
let _ = stdout_thread.join();
let _ = stderr_thread.join();
drop(tx);
while let Ok((stream, text)) = rx.recv() {
rep.on_event(BuildEvent::CompilerOutput {
stream,
text: &text,
});
}
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);
let inputs = expand_glob(&input_pattern)?;
if inputs.is_empty() {
continue;
}
let needs_stem = step.output.contains("{stem}");
if inputs.len() > 1 && !needs_stem {
return Err(format!(
"build.steps '{}' output must include {{stem}} for multiple inputs",
step.name
));
}
for input in inputs {
if !input.is_file() {
continue;
}
let stem = input.file_stem().and_then(|v| v.to_str()).unwrap_or("");
let out_path = PathBuf::from(expand_step_value(&step.output, stem, vars));
if should_run_step(&input, &out_path) {
return Ok(true);
}
}
}
Ok(false)
}
/// Creates VersionInfo from StepVars.
fn make_version_info(vars: &StepVars) -> VersionInfo {
VersionInfo {
full: vars.version.to_string(),
major: vars.version_major.to_string(),
minor: vars.version_minor.to_string(),
patch: vars.version_patch.to_string(),
suffix: vars.version_suffix.to_string(),
suffix_dash: vars.version_suffix_dash.to_string(),
}
}
/// 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, "");
s.replace("{stem}", stem)
}

View File

@@ -21,6 +21,7 @@ use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use toml::Value; use toml::Value;
use toml::map::Map; use toml::map::Map;
use toml_edit::{DocumentMut, Item, Table};
const DEFAULT_VERSION: &str = "0.1.0"; const DEFAULT_VERSION: &str = "0.1.0";
const DEFAULT_LANGUAGE: &str = "c"; const DEFAULT_LANGUAGE: &str = "c";
@@ -35,13 +36,16 @@ const VALID_KINDS: &[&str] = &[
"elf", "elf",
"none", "none",
"custom", "custom",
"flat-bin",
]; ];
/// Represents errors that can occur during configuration loading, parsing, or validation.
#[derive(Debug)] #[derive(Debug)]
pub enum ConfigError { pub enum ConfigError {
Io(std::io::Error), Io(std::io::Error),
TomlDe(toml::de::Error), TomlDe(toml::de::Error),
TomlSer(toml::ser::Error), TomlSer(toml::ser::Error),
TomlEdit(toml_edit::TomlError),
Invalid(String), Invalid(String),
} }
@@ -51,6 +55,7 @@ impl std::fmt::Display for ConfigError {
ConfigError::Io(err) => write!(f, "I/O error: {err}"), ConfigError::Io(err) => write!(f, "I/O error: {err}"),
ConfigError::TomlDe(err) => write!(f, "TOML parse error: {err}"), ConfigError::TomlDe(err) => write!(f, "TOML parse error: {err}"),
ConfigError::TomlSer(err) => write!(f, "TOML serialize error: {err}"), ConfigError::TomlSer(err) => write!(f, "TOML serialize error: {err}"),
ConfigError::TomlEdit(err) => write!(f, "TOML parse error: {err}"),
ConfigError::Invalid(msg) => write!(f, "Invalid config: {msg}"), ConfigError::Invalid(msg) => write!(f, "Invalid config: {msg}"),
} }
} }
@@ -76,13 +81,19 @@ impl From<toml::ser::Error> for ConfigError {
} }
} }
/// Core configuration structure that manages TOML parsing, typed views, and validation for DCR projects.
pub struct Config { pub struct Config {
path: PathBuf, path: PathBuf,
data: Value, data: Value,
typed: DcrConfig, typed: DcrConfig,
/// Editable document mirror of `data`, used for serialization. It preserves
/// comments, formatting, and any keys not modeled by the typed/raw views, so
/// `save()` never drops unknown sections (e.g. `[run]`, `[[build.post_steps]]`).
doc: DocumentMut,
} }
#[allow(dead_code)] #[allow(dead_code)]
/// Top-level typed configuration parsed from TOML, containing optional sections for package, build, dependencies, etc.
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct DcrConfig { pub struct DcrConfig {
#[serde(default)] #[serde(default)]
@@ -95,24 +106,66 @@ pub struct DcrConfig {
pub toolchain: Option<ToolchainConfig>, pub toolchain: Option<ToolchainConfig>,
#[serde(default)] #[serde(default)]
pub workspace: BTreeMap<String, WorkspaceMemberConfig>, pub workspace: BTreeMap<String, WorkspaceMemberConfig>,
#[serde(default)]
pub archive: Option<ArchiveConfig>,
} }
#[allow(dead_code)]
/// Configuration for archive output formats (e.g. EFI, bootable images).
#[derive(Debug, Clone, Deserialize)]
pub struct ArchiveConfig {
pub output: String,
pub format: String,
pub size: Option<String>,
pub offset: Option<String>,
pub label: Option<String>,
pub bootsector: Option<String>,
#[serde(default)]
pub layout: Vec<ArchiveLayout>,
}
#[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)] #[derive(Debug, Clone, Deserialize)]
pub struct PackageConfig { pub struct PackageConfig {
pub name: String, pub name: String,
pub version: 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")] #[serde(default, rename = "type")]
pub pkg_type: Option<String>, pub pkg_type: Option<String>,
} }
fn default_language() -> LanguageConfig {
LanguageConfig::One(DEFAULT_LANGUAGE.to_string())
}
fn default_compiler() -> String {
DEFAULT_COMPILER.to_string()
}
#[allow(dead_code)] #[allow(dead_code)]
/// Build configuration section, including language, standards, compiler, and output settings.
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct BuildConfig { pub struct BuildConfig {
#[serde(default = "default_language")]
pub language: LanguageConfig, pub language: LanguageConfig,
#[serde(default)] #[serde(default)]
pub standard: Option<String>, pub standard: Option<String>,
#[serde(default)] #[serde(default)]
pub cxx_standard: Option<String>, pub cxx_standard: Option<String>,
#[serde(default = "default_compiler")]
pub compiler: String, pub compiler: String,
#[serde(default)] #[serde(default)]
pub kind: Option<String>, pub kind: Option<String>,
@@ -138,8 +191,11 @@ pub struct BuildConfig {
pub src_disable: Option<bool>, pub src_disable: Option<bool>,
#[serde(default)] #[serde(default)]
pub workspace_only: bool, pub workspace_only: bool,
#[serde(default)]
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)] #[derive(Debug, Clone, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum LanguageConfig { pub enum LanguageConfig {
@@ -156,6 +212,7 @@ impl LanguageConfig {
} }
} }
/// Dependency configuration can be a version string or a full table of dependency details.
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
@@ -164,6 +221,7 @@ pub enum DependencyConfig {
Table(BTreeMap<String, Value>), Table(BTreeMap<String, Value>),
} }
/// Toolchain configuration for custom compilers, assemblers, and linkers.
#[derive(Debug, Clone, Deserialize, Default)] #[derive(Debug, Clone, Deserialize, Default)]
pub struct ToolchainConfig { pub struct ToolchainConfig {
pub cc: Option<String>, pub cc: Option<String>,
@@ -174,53 +232,143 @@ pub struct ToolchainConfig {
pub ld: Option<String>, pub ld: Option<String>,
} }
/// Workspace member configuration for multi-project workspaces.
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct WorkspaceMemberConfig { pub struct WorkspaceMemberConfig {
pub path: String, pub path: String,
#[serde(default)] #[serde(default)]
pub deps: Vec<String>, pub deps: Vec<String>,
#[serde(default)]
pub main: Option<bool>,
} }
impl Config { 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> { 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 path = PathBuf::from(path);
let data = if path.exists() { let (data, doc) = if path.exists() {
read_toml(&path)? load_parts(&path)?
} else { } else {
let default_value = default_toml()?; let content = default_toml_text(package_name)?;
write_toml(&path, &default_value)?; let doc: DocumentMut = content.parse().map_err(ConfigError::TomlEdit)?;
default_value fs::write(&path, doc.to_string())?;
let data: Value = toml::from_str(&content)?;
(data, doc)
}; };
let typed = parse_typed_config(&data)?; let typed = parse_typed_config(&data)?;
let cfg = Self { path, data, typed }; let cfg = Self {
path,
data,
typed,
doc,
};
cfg.validate()?; cfg.validate()?;
cfg.warn_dcr_version_mismatch();
Ok(cfg) 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> { pub fn open(path: &str) -> Result<Self, ConfigError> {
let path = PathBuf::from(path); let path = PathBuf::from(path);
if !path.exists() { if !path.exists() {
return Err(ConfigError::Invalid("dcr.toml not found".into())); return Err(ConfigError::Invalid("dcr.toml not found".into()));
} }
let data = read_toml(&path)?; let (data, doc) = load_parts(&path)?;
let typed = parse_typed_config(&data)?; let typed = parse_typed_config(&data)?;
let cfg = Self { path, data, typed }; let cfg = Self {
path,
data,
typed,
doc,
};
cfg.validate()?; cfg.validate()?;
cfg.warn_dcr_version_mismatch();
Ok(cfg) Ok(cfg)
} }
#[allow(dead_code)] #[allow(dead_code)]
/// Returns the fully typed configuration struct.
pub fn typed(&self) -> &DcrConfig { pub fn typed(&self) -> &DcrConfig {
&self.typed &self.typed
} }
#[allow(dead_code)] #[allow(dead_code)]
/// Returns the package configuration if present.
pub fn package(&self) -> Option<&PackageConfig> { pub fn package(&self) -> Option<&PackageConfig> {
self.typed.package.as_ref() 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 {
crate::utils::log::warn(&format!(
"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) => {
crate::utils::log::warn(&format!(
"this project requires dcr {required} (package.dcr-version), \
but you are running dcr {tool}. Some features may be missing or misbehave. \
Upgrade dcr (e.g. `dcr --update`) or lower package.dcr-version if intentional."
));
}
Some(std::cmp::Ordering::Greater) => {
crate::utils::log::warn(&format!(
"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 => {
crate::utils::log::warn(&format!(
"package.dcr-version = \"{required}\" is not a valid semver (expected X.Y.Z); ignoring"
));
}
}
}
#[allow(dead_code)] #[allow(dead_code)]
/// Returns the build configuration if present.
pub fn build_config(&self) -> Option<&BuildConfig> { pub fn build_config(&self) -> Option<&BuildConfig> {
self.typed.build.as_ref() self.typed.build.as_ref()
} }
@@ -234,7 +382,7 @@ impl Config {
&& !self.typed.workspace.is_empty() && !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). /// Used for workspace member inheritance (build.inherit = true).
pub fn merge_parent(&mut self, parent: &Config) { pub fn merge_parent(&mut self, parent: &Config) {
let fields: &[&str] = &[ let fields: &[&str] = &[
@@ -312,24 +460,29 @@ impl Config {
} }
} }
/// Retrieves a nested value from the config data using dot-separated path.
pub fn get(&self, key: &str) -> Option<&Value> { pub fn get(&self, key: &str) -> Option<&Value> {
let parts: Vec<&str> = key.split('.').collect(); let parts: Vec<&str> = key.split('.').collect();
get_path(&self.data, &parts) get_path(&self.data, &parts)
} }
#[allow(dead_code)] #[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> { pub fn add(&mut self, key: &str, value: Value) -> Result<(), ConfigError> {
self.set(key, value) self.set(key, value)
} }
/// Updates a configuration value at the given key path.
pub fn edit(&mut self, key: &str, value: Value) -> Result<(), ConfigError> { pub fn edit(&mut self, key: &str, value: Value) -> Result<(), ConfigError> {
self.set(key, value) self.set(key, value)
} }
#[allow(dead_code)] #[allow(dead_code)]
/// Checks if the configuration is valid without returning the error.
pub fn check(&self) -> bool { pub fn check(&self) -> bool {
self.validate().is_ok() self.validate().is_ok()
} }
/// Performs comprehensive validation of the configuration, including package, build, toolchain, and workspace rules.
pub fn validate(&self) -> Result<(), ConfigError> { pub fn validate(&self) -> Result<(), ConfigError> {
if self.is_workspace_only() { if self.is_workspace_only() {
return self.validate_workspace(); return self.validate_workspace();
@@ -345,6 +498,13 @@ impl Config {
.as_ref() .as_ref()
.ok_or_else(|| ConfigError::Invalid("missing [build] section (required)".into()))?; .ok_or_else(|| ConfigError::Invalid("missing [build] section (required)".into()))?;
// For workspace_only projects, [build] section is optional
// and language/compiler are not required
if build.workspace_only {
self.validate_workspace()?;
return Ok(());
}
if pkg.name.trim().is_empty() { if pkg.name.trim().is_empty() {
return Err(ConfigError::Invalid("package.name is empty".into())); return Err(ConfigError::Invalid("package.name is empty".into()));
} }
@@ -419,22 +579,35 @@ impl Config {
} }
pub fn save(&self) -> Result<(), ConfigError> { pub fn save(&self) -> Result<(), ConfigError> {
write_toml(&self.path, &self.data) fs::write(&self.path, self.doc.to_string())?;
Ok(())
} }
fn set(&mut self, key: &str, value: Value) -> Result<(), ConfigError> { fn set(&mut self, key: &str, value: Value) -> Result<(), ConfigError> {
let parts: Vec<&str> = key.split('.').collect(); let parts: Vec<&str> = key.split('.').collect();
let previous = self.data.clone(); let previous_data = self.data.clone();
set_path(&mut self.data, &parts, value)?; let previous_doc = self.doc.clone();
if let Err(err) = set_doc_path(&mut self.doc, &parts, &value) {
self.doc = previous_doc;
return Err(err);
}
if let Err(err) = set_path(&mut self.data, &parts, value) {
self.data = previous_data;
self.doc = previous_doc;
return Err(err);
}
self.typed = match parse_typed_config(&self.data) { self.typed = match parse_typed_config(&self.data) {
Ok(typed) => typed, Ok(typed) => typed,
Err(err) => { Err(err) => {
self.data = previous; self.data = previous_data;
self.doc = previous_doc;
return Err(err); return Err(err);
} }
}; };
if let Err(err) = self.validate() { if let Err(err) = self.validate() {
self.data = previous; self.data = previous_data;
self.doc = previous_doc;
self.typed = parse_typed_config(&self.data)?; self.typed = parse_typed_config(&self.data)?;
return Err(err); return Err(err);
} }
@@ -656,235 +829,138 @@ impl Config {
} }
} }
fn read_toml(path: &Path) -> Result<Value, ConfigError> { fn load_parts(path: &Path) -> Result<(Value, DocumentMut), ConfigError> {
let content = fs::read_to_string(path)?; let content = fs::read_to_string(path)?;
Ok(toml::from_str(&content)?) let data: Value = toml::from_str(&content)?;
let doc: DocumentMut = content.parse().map_err(ConfigError::TomlEdit)?;
Ok((data, doc))
} }
fn write_toml(path: &Path, value: &Value) -> Result<(), ConfigError> { fn default_toml_text(package_name: Option<&str>) -> Result<String, ConfigError> {
let content = format_toml(value)?; let name = package_name
fs::write(path, content)?; .map(|s| s.to_string())
Ok(()) .or_else(|| {
} std::env::current_dir()
fn default_toml() -> Result<Value, ConfigError> {
let name = std::env::current_dir()
.ok() .ok()
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string())) .and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string()))
})
.unwrap_or_else(|| "project".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");
let mut package = Map::new(); Ok(format!(
package.insert("name".to_string(), Value::String(name)); "[package]\n\
package.insert( name = \"{name}\"\n\
"version".to_string(), version = \"{DEFAULT_VERSION}\"\n\
Value::String(DEFAULT_VERSION.to_string()), dcr-version = \"{dcr_version}\"\n\
); type = \"none\"\n\
package.insert("type".to_string(), Value::String("none".to_string())); description = \"\"\n\
package.insert("description".to_string(), Value::String("".to_string())); author = \"\"\n\
package.insert("author".to_string(), Value::String("".to_string())); license = \"GPL-3.0-or-later\"\n\
package.insert( \n\
"license".to_string(), [build]\n\
Value::String("GPL-3.0-or-later".to_string()), language = \"{DEFAULT_LANGUAGE}\"\n\
); standard = \"{DEFAULT_STANDARD}\"\n\
compiler = \"{DEFAULT_COMPILER}\"\n\
let mut build = Map::new(); kind = \"{DEFAULT_KIND}\"\n\
build.insert( \n\
"language".to_string(), [dependencies]\n"
Value::String(DEFAULT_LANGUAGE.to_string()), ))
);
build.insert(
"standard".to_string(),
Value::String(DEFAULT_STANDARD.to_string()),
);
build.insert(
"compiler".to_string(),
Value::String(DEFAULT_COMPILER.to_string()),
);
build.insert("kind".to_string(), Value::String(DEFAULT_KIND.to_string()));
let mut root = Map::new();
root.insert("package".to_string(), Value::Table(package));
root.insert("build".to_string(), Value::Table(build));
root.insert("dependencies".to_string(), Value::Table(Map::new()));
Ok(Value::Table(root))
} }
fn format_toml(value: &Value) -> Result<String, ConfigError> { fn dcr_version_warn_once(path_key: &str) -> bool {
let root = value use std::collections::HashSet;
.as_table() use std::sync::{Mutex, OnceLock};
.ok_or_else(|| ConfigError::Invalid("root is not a table".into()))?; static WARNED: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
let set = WARNED.get_or_init(|| Mutex::new(HashSet::new()));
let package = root let Ok(mut guard) = set.lock() else {
.get("package") return true;
.and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [package]".into()))?;
let build = root
.get("build")
.and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [build]".into()))?;
let deps = root
.get("dependencies")
.and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [dependencies]".into()))?;
let toolchain = root.get("toolchain").and_then(|v| v.as_table());
let name = package.get("name").and_then(|v| v.as_str()).unwrap_or("");
let version = package
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("");
let language_value = build.get("language");
let language = match language_value {
Some(Value::String(s)) => s.to_string(),
Some(Value::Array(arr)) => format_string_array(&Value::Array(arr.clone())),
_ => "".to_string(),
}; };
let standard = build.get("standard").and_then(|v| v.as_str()).unwrap_or(""); guard.insert(path_key.to_string())
let compiler = build.get("compiler").and_then(|v| v.as_str()).unwrap_or("");
let mut out = String::new();
out.push_str("[package]\n");
out.push_str(&format!("name = \"{name}\"\n"));
out.push_str(&format!("version = \"{version}\"\n"));
for key in [
"type",
"description",
"author",
"authors",
"homepage",
"license",
"repository",
"readme",
"keywords",
"categories",
] {
if let Some(val) = package.get(key) {
match val {
Value::String(s) => out.push_str(&format!("{key} = \"{s}\"\n")),
Value::Array(_) => out.push_str(&format!("{key} = {}\n", format_string_array(val))),
_ => {}
} }
}
}
out.push('\n');
out.push_str("[build]\n"); fn set_doc_path(doc: &mut DocumentMut, path: &[&str], value: &Value) -> Result<(), ConfigError> {
if language.starts_with('[') { let mut current = doc.as_table_mut();
out.push_str(&format!("language = {language}\n")); for &key in &path[..path.len().saturating_sub(1)] {
if !current.contains_key(key) {
current.insert(key, Item::Table(Table::new()));
}
current = current
.get_mut(key)
.and_then(|item| item.as_table_mut())
.ok_or_else(|| ConfigError::Invalid(format!("'{key}' is not a table")))?;
}
if let Some(&last) = path.last() {
current.insert(last, value_to_item(value));
Ok(())
} else { } else {
out.push_str(&format!("language = \"{language}\"\n")); Err(ConfigError::Invalid("empty key".into()))
}
if !standard.is_empty() {
out.push_str(&format!("standard = \"{standard}\"\n"));
}
out.push_str(&format!("compiler = \"{compiler}\"\n"));
let kind = build
.get("kind")
.and_then(|v| v.as_str())
.unwrap_or(DEFAULT_KIND);
out.push_str(&format!("kind = \"{kind}\"\n"));
if let Some(target) = build.get("target").and_then(|v| v.as_str())
&& !target.trim().is_empty()
{
out.push_str(&format!("target = \"{target}\"\n"));
}
if let Some(out_dir) = build.get("out_dir").and_then(|v| v.as_str())
&& !out_dir.trim().is_empty()
{
out.push_str(&format!("out_dir = \"{out_dir}\"\n"));
}
if let Some(cflags) = build.get("cflags") {
out.push_str(&format!("cflags = {}\n", format_string_array(cflags)));
}
if let Some(ldflags) = build.get("ldflags") {
out.push_str(&format!("ldflags = {}\n", format_string_array(ldflags)));
}
out.push('\n');
if let Some(toolchain) = toolchain {
let mut lines = Vec::new();
for key in ["cc", "cxx", "as", "ar", "ld"] {
if let Some(value) = toolchain.get(key).and_then(|v| v.as_str())
&& !value.trim().is_empty()
{
lines.push(format!("{key} = \"{value}\""));
}
}
if !lines.is_empty() {
out.push_str("[toolchain]\n");
for line in lines {
out.push_str(&format!("{line}\n"));
}
out.push('\n');
} }
} }
out.push_str("[dependencies]\n"); fn value_to_item(value: &Value) -> Item {
if !deps.is_empty() { Item::Value(value_to_edit_value(value))
let mut keys: Vec<&String> = deps.keys().collect();
keys.sort();
for key in keys {
if let Some(val) = deps.get(key) {
out.push_str(&format!("{key} = {}\n", format_dep_value(val)));
}
}
}
Ok(out)
} }
fn format_dep_value(value: &Value) -> String { fn value_to_edit_value(value: &Value) -> toml_edit::Value {
match value { match value {
Value::String(s) => format!("\"{s}\""), Value::String(s) => toml_edit::Value::from(s.clone()),
Value::Integer(i) => toml_edit::Value::from(*i),
Value::Float(f) => toml_edit::Value::from(*f),
Value::Boolean(b) => toml_edit::Value::from(*b),
Value::Datetime(dt) => {
let s = dt.to_string();
s.parse::<toml_edit::Datetime>()
.map(toml_edit::Value::from)
.unwrap_or_else(|_| toml_edit::Value::from(s))
}
Value::Array(arr) => {
let mut out = toml_edit::Array::new();
for item in arr {
out.push(value_to_edit_value(item));
}
toml_edit::Value::Array(out)
}
Value::Table(tbl) => { Value::Table(tbl) => {
let mut parts = Vec::new(); let mut out = toml_edit::InlineTable::new();
if let Some(v) = tbl.get("version").and_then(|v| v.as_str()) { for key in ordered_table_keys(tbl) {
parts.push(format!("version = \"{v}\"")); if let Some(v) = tbl.get(&key) {
out.insert(&key, value_to_edit_value(v));
} }
if let Some(v) = tbl.get("path").and_then(|v| v.as_str()) {
parts.push(format!("path = \"{v}\""));
} }
if let Some(v) = tbl.get("git").and_then(|v| v.as_str()) { toml_edit::Value::InlineTable(out)
parts.push(format!("git = \"{v}\""));
} }
if let Some(v) = tbl.get("branch").and_then(|v| v.as_str()) {
parts.push(format!("branch = \"{v}\""));
}
if let Some(v) = tbl.get("tag").and_then(|v| v.as_str()) {
parts.push(format!("tag = \"{v}\""));
}
if let Some(v) = tbl.get("rev").and_then(|v| v.as_str()) {
parts.push(format!("rev = \"{v}\""));
}
if let Some(v) = tbl.get("default-features").and_then(|v| v.as_bool()) {
parts.push(format!(
"default-features = {}",
if v { "true" } else { "false" }
));
}
if let Some(v) = tbl.get("features") {
parts.push(format!("features = {}", format_string_array(v)));
}
if let Some(v) = tbl.get("system").and_then(|v| v.as_bool()) {
parts.push(format!("system = {}", if v { "true" } else { "false" }));
}
format!("{{ {} }}", parts.join(", "))
}
_ => "\"\"".to_string(),
} }
} }
fn format_string_array(value: &Value) -> String { const DEP_KEY_ORDER: &[&str] = &[
if let Some(arr) = value.as_array() { "version",
let items: Vec<String> = arr "path",
"git",
"branch",
"tag",
"rev",
"default-features",
"features",
"system",
];
fn ordered_table_keys(tbl: &Map<String, Value>) -> Vec<String> {
let mut keys: Vec<String> = DEP_KEY_ORDER
.iter() .iter()
.filter_map(|v| v.as_str().map(|s| format!("\"{s}\""))) .filter(|k| tbl.contains_key(**k))
.map(|k| (*k).to_string())
.collect(); .collect();
return format!("[{}]", items.join(", ")); let mut rest: Vec<String> = tbl
} .keys()
"[]".to_string() .filter(|k| !DEP_KEY_ORDER.contains(&k.as_str()))
.cloned()
.collect();
rest.sort();
keys.extend(rest);
keys
} }
fn get_path<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Value> { fn get_path<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Value> {
@@ -956,11 +1032,21 @@ mod tests {
let dir = temp_dir("typed_config"); let dir = temp_dir("typed_config");
let path = write_toml_file( let path = write_toml_file(
&dir, &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(); let config = Config::open(&path.to_string_lossy()).unwrap();
assert_eq!(config.package().unwrap().name, "typed"); 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().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().compiler, "clang");
assert_eq!(config.build_config().unwrap().cflags, ["-Wall"]); assert_eq!(config.build_config().unwrap().cflags, ["-Wall"]);
assert!(config.typed().dependencies.contains_key("foo")); assert!(config.typed().dependencies.contains_key("foo"));
@@ -1133,6 +1219,100 @@ mod tests {
); );
} }
#[test]
fn save_preserves_untyped_sections() {
let dir = temp_dir("preserve_untyped");
let path = write_toml_file(
&dir,
"# top comment\n\
[package]\n\
name = \"test\"\n\
version = \"0.1.0\"\n\n\
[build]\n\
language = \"c\"\n\
standard = \"c11\"\n\
compiler = \"clang\"\n\
kind = \"elf\"\n\
filename = \"KERNEL\"\n\
extension = \"ELF\"\n\
inherit = false\n\
include = [\"src/include\"]\n\n\
[[build.post_steps]]\n\
name = \"iso\"\n\
cmd = \"grub-mkrescue\"\n\n\
[run]\n\
cmd = \"qemu-system-aarch64 -kernel KERNEL\"\n\n\
[dependencies]\n",
);
let mut config = Config::open(&path.to_string_lossy()).unwrap();
config
.set("dependencies.zlib", {
let mut tbl = Map::new();
tbl.insert("path".to_string(), Value::String("../zlib".to_string()));
Value::Table(tbl)
})
.unwrap();
// Reload from disk and confirm nothing modeled-but-not-whitelisted was dropped.
let reloaded = Config::open(&path.to_string_lossy()).unwrap();
assert_eq!(
reloaded.get("build.filename").and_then(|v| v.as_str()),
Some("KERNEL"),
"build.filename must survive save"
);
assert_eq!(
reloaded.get("build.extension").and_then(|v| v.as_str()),
Some("ELF")
);
assert_eq!(
reloaded.get("build.inherit").and_then(|v| v.as_bool()),
Some(false)
);
assert!(
reloaded
.get("build.include")
.and_then(|v| v.as_array())
.is_some(),
"build.include array must survive save"
);
assert_eq!(
reloaded.get("run.cmd").and_then(|v| v.as_str()),
Some("qemu-system-aarch64 -kernel KERNEL"),
"[run] section must survive save"
);
assert!(
reloaded.get("build.post_steps").is_some(),
"[[build.post_steps]] must survive save"
);
let saved = fs::read_to_string(&path).unwrap();
assert!(
saved.contains("zlib = { path = \"../zlib\" }"),
"dependency must be an inline table, got:\n{saved}"
);
assert!(
saved.contains("# top comment"),
"leading comment must be preserved, got:\n{saved}"
);
}
#[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] #[test]
fn new_creates_default_config() { fn new_creates_default_config() {
let dir = temp_dir("new_default"); let dir = temp_dir("new_default");

View File

@@ -1,183 +0,0 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform;
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let lang = ctx.language.to_lowercase();
if lang.split(',').any(|p| p.trim() != "asm") {
return Err("GAS backend requires build.language = \"asm\"".to_string());
}
let assembler = if ctx.compiler.is_empty() {
"as"
} else {
ctx.compiler
};
let start_time = Instant::now();
let sources = collect_sources(ctx)?;
let obj_dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
let objects = build_objects(assembler, &sources, &obj_dir, ctx, "o")?;
if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
if !common::needs_link(&objects, &lib_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") {
"lib"
} else {
"ar"
});
let mut cmd = Command::new(archiver);
if cfg!(target_os = "windows") && archiver == "lib" {
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
} else {
cmd.arg("rcs").arg(&lib_path);
}
for obj in &objects {
cmd.arg(obj);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
let mut cmd = Command::new(ctx.linker.unwrap_or("cc"));
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
for obj in &objects {
cmd.arg(obj);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("-L{dir}"));
}
for lib in ctx.libs {
cmd.arg(format!("-l{lib}"));
}
for flag in ctx.ldflags {
cmd.arg(flag);
}
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
format!("{}.{}", name, ext)
};
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "elf" {
platform::elf_path(ctx.profile, &final_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
};
if !common::needs_link(&objects, &out_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
cmd.arg("-o").arg(out_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
Ok(elapsed)
}
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
// GAS handles only lowercase .s files
common::collect_sources(
ctx.source_roots,
&["s"],
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn build_objects(
assembler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
common::parallel_build(sources.len(), |i| {
build_object(assembler, &sources[i], &objects[i], ctx)
})?;
Ok(objects)
}
fn build_object(
assembler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(assembler);
cmd.arg(source).arg("-o").arg(obj_path);
for flag in ctx.cflags {
cmd.arg(flag);
}
common::run_command_sync_output(&mut cmd)
}

View File

@@ -1,94 +0,0 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod common;
pub mod gas;
pub mod msvc;
pub mod nasm;
pub mod unix_cc;
pub struct BuildContext<'a> {
pub profile: &'a str,
pub project_name: &'a str,
pub compiler: &'a str,
pub language: &'a str,
pub standard: &'a str,
pub cxx_standard: &'a str,
pub target: Option<&'a str>,
pub target_dir: Option<&'a str>,
pub kind: &'a str,
pub platform: Option<&'a str>,
pub linker: Option<&'a str>,
pub archiver: Option<&'a str>,
pub package_type: Option<&'a str>,
pub source_roots: &'a [std::path::PathBuf],
pub exclude_dirs: &'a [std::path::PathBuf],
pub include_paths: &'a [String],
pub include_dirs: &'a [String],
pub lib_dirs: &'a [String],
pub libs: &'a [String],
pub cflags: &'a [String],
pub ldflags: &'a [String],
pub output_filename: Option<&'a str>,
pub output_extension: Option<&'a str>,
pub verbose: bool,
}
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let compiler = ctx.compiler.to_lowercase();
if !check_compiler_exists(ctx.compiler) {
return Err(format!(
"Compiler not found: {}. Make sure it is installed and available in PATH.",
ctx.compiler
));
}
if compiler.contains("clang-cl") {
return msvc::build(ctx);
}
if compiler == "as" || compiler.contains("gas") {
return gas::build(ctx);
}
if compiler.contains("nasm") {
return nasm::build(ctx);
}
if compiler == "cl" || compiler.contains("msvc") {
return msvc::build(ctx);
}
unix_cc::build(ctx)
}
fn check_compiler_exists(compiler: &str) -> bool {
let name = if compiler.is_empty() { "cc" } else { compiler };
std::process::Command::new(name)
.arg("--version")
.output()
.is_ok()
}
pub fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
let compiler = ctx.compiler.to_lowercase();
if compiler.contains("clang-cl") || compiler == "cl" || compiler.contains("msvc") {
return msvc::collect_sources(ctx);
}
if compiler == "as" || compiler.contains("gas") {
return gas::collect_sources(ctx);
}
if compiler.contains("nasm") {
return nasm::collect_sources(ctx);
}
unix_cc::collect_sources(ctx)
}

View File

@@ -1,230 +0,0 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform;
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let lang = ctx.language.to_lowercase();
if lang.split(',').any(|p| p.trim() != "asm") {
return Err("NASM backend requires build.language = \"asm\"".to_string());
}
let assembler = if ctx.compiler.is_empty() {
"nasm"
} else {
ctx.compiler
};
let start_time = Instant::now();
let sources = collect_sources(ctx)?;
let obj_dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
let objects = build_objects(assembler, &sources, &obj_dir, ctx, "o")?;
if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
if !common::needs_link(&objects, &lib_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") {
"lib"
} else {
"ar"
});
let mut cmd = Command::new(archiver);
if cfg!(target_os = "windows") && archiver == "lib" {
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
} else {
cmd.arg("rcs").arg(&lib_path);
}
for obj in &objects {
cmd.arg(obj);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
let mut cmd = Command::new(ctx.linker.unwrap_or("cc"));
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
for obj in &objects {
cmd.arg(obj);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("-L{dir}"));
}
for lib in ctx.libs {
cmd.arg(format!("-l{lib}"));
}
for flag in ctx.ldflags {
cmd.arg(flag);
}
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
format!("{}.{}", name, ext)
};
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "elf" {
platform::elf_path(ctx.profile, &final_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
};
if !common::needs_link(&objects, &out_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
cmd.arg("-o").arg(out_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
Ok(elapsed)
}
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
// NASM handles lowercase .s and .asm only (not .S which is GCC preprocessed)
common::collect_sources(
ctx.source_roots,
&["asm", "s"],
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn build_objects(
assembler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
let format = nasm_format(ctx.platform);
common::parallel_build(sources.len(), |i| {
build_object(assembler, &sources[i], &objects[i], format, ctx)
})?;
Ok(objects)
}
fn build_object(
assembler: &str,
source: &str,
obj_path: &str,
format: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(assembler);
cmd.arg("-f")
.arg(format)
.arg(source)
.arg("-o")
.arg(obj_path);
for flag in ctx.cflags {
cmd.arg(flag);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
common::run_command_sync_output(&mut cmd)
}
fn nasm_format(platform: Option<&str>) -> &'static str {
if let Some(p) = platform {
let p = p.to_lowercase().replace('-', "_");
if p == "x86" || (p.starts_with('i') && p.ends_with("86") && p.len() == 4) {
#[cfg(target_os = "macos")]
{
return "macho32";
}
#[cfg(target_os = "windows")]
{
return "win32";
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
return "elf32";
}
}
}
#[cfg(target_os = "linux")]
{
"elf64"
}
#[cfg(target_os = "macos")]
{
"macho64"
}
#[cfg(target_os = "windows")]
{
"win64"
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
"elf64"
}
}

View File

@@ -1,285 +0,0 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform;
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let compiler = if ctx.compiler.is_empty() {
"cc"
} else {
ctx.compiler
};
let start_time = Instant::now();
let extensions = source_extensions(ctx.language);
let sources = common::collect_sources(
ctx.source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)?;
let obj_dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "o")?;
if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
if !common::needs_link(&objects, &lib_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
let mut cmd = Command::new(ctx.archiver.unwrap_or("ar"));
cmd.arg("rcs").arg(&lib_path);
for obj in &objects {
cmd.arg(obj);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
let mut cmd = Command::new(ctx.linker.unwrap_or(compiler));
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
if ctx.kind == "efi" {
cmd.arg("-shared");
cmd.arg("-nostdlib");
cmd.arg("-Wl,-dll");
cmd.arg("-Wl,--subsystem,10");
}
for obj in &objects {
cmd.arg(obj);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("-L{dir}"));
}
for lib in ctx.libs {
cmd.arg(format!("-l{lib}"));
}
for flag in ctx.ldflags {
cmd.arg(flag);
}
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
format!("{}.{}", name, ext)
};
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "efi" {
platform::efi_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "elf" {
platform::elf_path(ctx.profile, &final_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
};
if !common::needs_link(&objects, &out_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
cmd.arg("-o").arg(out_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
Ok(elapsed)
}
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
let extensions = source_extensions(ctx.language);
common::collect_sources(
ctx.source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn source_extensions(language: &str) -> Vec<&str> {
let mut out = Vec::new();
for part in language.split(',').map(|s| s.trim()) {
let lang = part.to_lowercase();
match lang.as_str() {
"c" => out.extend(["c"]),
"c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]),
"asm" => out.extend(["s", "S", "asm"]),
_ => {}
}
}
if out.is_empty() {
out.extend(["c"]);
}
out
}
fn default_flags(profile: &str) -> &'static [&'static str] {
match profile {
"release" => &["-O3", "-DNDEBUG"],
"debug" => &[
"-O0",
"-g",
"-Wall",
"-Wextra",
"-fno-omit-frame-pointer",
"-DDCR_DEBUG",
],
_ => &[],
}
}
fn is_bare_metal_target(target: Option<&str>) -> bool {
if let Some(t) = target {
let lower = t.to_lowercase();
lower.contains("none")
|| lower.contains("-elf")
|| lower.contains("eabi")
|| lower.contains("baremetal")
|| lower.contains("bare-metal")
} else {
false
}
}
fn build_objects(
compiler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
common::parallel_build(sources.len(), |i| {
build_object(compiler, &sources[i], &objects[i], ctx)
})?;
Ok(objects)
}
fn build_object(
compiler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(compiler);
cmd.arg("-c");
if let Some(flag) = asm_lang_flag(source) {
cmd.arg("-x").arg(flag);
}
cmd.arg(source).arg("-o").arg(obj_path);
if ctx.kind == "sharedlib" {
cmd.arg("-fPIC");
}
if let Some(platform) = ctx.platform
&& !platform.trim().is_empty()
{
cmd.arg(format!("-march={}", platform));
}
if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" {
let ext = Path::new(source)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let is_cpp = matches!(ext, "cpp" | "cxx" | "cc");
if is_cpp && !ctx.cxx_standard.is_empty() {
cmd.arg(format!("-std={}", ctx.cxx_standard));
} else if !is_cpp {
cmd.arg(format!("-std={}", ctx.standard));
}
}
let use_dcr_defaults = ctx.cflags.is_empty() && !is_bare_metal_target(ctx.target);
if use_dcr_defaults {
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.include_dirs {
cmd.arg(format!("-I{dir}"));
}
let d_path = Path::new(obj_path).with_extension("d");
cmd.arg("-MMD").arg("-MF").arg(&d_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
common::run_command_sync_output(&mut cmd)
}
fn asm_lang_flag(source: &str) -> Option<&'static str> {
let ext = Path::new(source).extension().and_then(|v| v.to_str())?;
match ext {
"S" => Some("assembler-with-cpp"),
"s" | "asm" => Some("assembler"),
_ => None,
}
}

View File

@@ -1,128 +1,10 @@
// DCR — Cargo-like C/C++ project manager. /// Represents resolved dependencies for a build target, including include directories, library directories, and libraries.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#[allow(dead_code)]
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum DepSource {
Path(String),
Git {
url: String,
branch: Option<String>,
tag: Option<String>,
rev: Option<String>,
},
Registry {
version: String,
features: Option<Vec<String>>,
},
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct DepSpec {
pub name: String,
pub source: DepSource,
pub include_raw: Option<Vec<String>>,
pub lib_raw: Option<Vec<String>>,
pub libs_raw: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ResolvedDeps { pub struct ResolvedDeps {
pub include_dirs: Vec<String>, pub include_dirs: Vec<String>,
pub lib_dirs: Vec<String>, pub lib_dirs: Vec<String>,
pub libs: Vec<String>, pub libs: Vec<String>,
} pub cflags: Vec<String>,
pub ldflags: Vec<String>,
#[allow(dead_code)] pub package_roots: Vec<String>,
pub fn resolve_path(project_root: &Path, raw: &str) -> Result<PathBuf, String> {
let p = Path::new(raw);
let full = if p.is_absolute() {
p.to_path_buf()
} else {
project_root.join(p)
};
Ok(full)
}
#[allow(dead_code)]
pub fn resolve_paths(
base: &Path,
raw: Option<&[String]>,
defaults: &[&str],
profile: &str,
) -> Result<Vec<PathBuf>, String> {
let mut out = Vec::new();
if let Some(raw) = raw {
for r in raw {
let expanded = expand_profile(r, profile);
let p = Path::new(&expanded);
let full = if p.is_absolute() {
p.to_path_buf()
} else {
base.join(p)
};
if !full.exists() {
return Err(format!("Path does not exist: {}", full.display()));
}
out.push(full);
}
return Ok(out);
}
for d in defaults {
let candidate = base.join(d);
if candidate.exists() {
out.push(candidate);
}
}
Ok(out)
}
#[allow(dead_code)]
pub fn expand_profile(raw: &str, profile: &str) -> String {
raw.replace("{profile}", profile)
}
#[allow(dead_code)]
pub fn sync_dep_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
if dst.exists() {
fs::remove_dir_all(dst)?;
}
copy_dir_all(src, dst)
}
#[allow(dead_code)]
pub fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
let from = entry.path();
let to = dst.join(entry.file_name());
if ty.is_dir() {
copy_dir_all(&from, &to)?;
} else {
fs::copy(&from, &to)?;
}
}
Ok(())
} }

View File

@@ -15,11 +15,10 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use sha2::{Digest, Sha256};
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use toml::Value;
/// Represents a locked dependency entry containing its name, version, source, and checksum.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DepLock { pub struct DepLock {
pub name: String, pub name: String,
@@ -28,6 +27,7 @@ pub struct DepLock {
pub source: String, 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( pub fn write_lock(
project_root: &Path, project_root: &Path,
project_name: &str, project_name: &str,
@@ -49,6 +49,7 @@ pub fn write_lock(
} }
out.push('\n'); out.push('\n');
// Write metadata for each package in the dependencies list
for pkg in packages { for pkg in packages {
out.push_str("[[package]]\n"); out.push_str("[[package]]\n");
out.push_str(&format!("name = \"{}\"\n", escape_value(&pkg.name))); out.push_str(&format!("name = \"{}\"\n", escape_value(&pkg.name)));
@@ -62,6 +63,7 @@ pub fn write_lock(
Ok(()) Ok(())
} }
/// Converts a list of strings into a comma-separated TOML array by quoting each item.
fn quote_list(items: &[String]) -> String { fn quote_list(items: &[String]) -> String {
items items
.iter() .iter()
@@ -70,68 +72,7 @@ fn quote_list(items: &[String]) -> String {
.join(", ") .join(", ")
} }
/// Escapes backslashes and double quotes in a string to make it safe for TOML.
fn escape_value(input: &str) -> String { fn escape_value(input: &str) -> String {
input.replace('\\', "\\\\").replace('"', "\\\"") input.replace('\\', "\\\\").replace('"', "\\\"")
} }
#[allow(dead_code)]
pub fn read_dep_version(dep_path: &Path) -> Option<String> {
let path = dep_path.join("dcr.toml");
let content = fs::read_to_string(path).ok()?;
let value: Value = content.parse().ok()?;
value
.get("package")
.and_then(|v| v.as_table())
.and_then(|t| t.get("version"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
#[allow(dead_code)]
pub fn compute_checksum(root: &Path) -> Result<String, String> {
let mut files = Vec::new();
collect_files(root, root, &mut files)?;
files.sort();
let mut hasher = Sha256::new();
for rel in files {
hasher.update(rel.as_bytes());
let data =
fs::read(root.join(&rel)).map_err(|err| format!("failed to read {}: {err}", rel))?;
hasher.update(&data);
}
let hash = hasher.finalize();
Ok(to_hex(&hash))
}
#[allow(dead_code)]
fn collect_files(root: &Path, dir: &Path, out: &mut Vec<String>) -> Result<(), String> {
for entry in fs::read_dir(dir).map_err(|err| format!("read_dir failed: {err}"))? {
let entry = entry.map_err(|err| format!("read_dir failed: {err}"))?;
let path = entry.path();
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str == "target" {
continue;
}
if path.is_dir() {
collect_files(root, &path, out)?;
} else if path.is_file() {
let rel = path
.strip_prefix(root)
.unwrap_or(&path)
.to_string_lossy()
.to_string();
out.push(rel);
}
}
Ok(())
}
#[allow(dead_code)]
fn to_hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push_str(&format!("{:02x}", b));
}
out
}

View File

@@ -15,16 +15,18 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
/// Core dependency resolver for DCR projects.
pub mod common; pub mod common;
pub mod git;
pub mod lock; pub mod lock;
pub mod register; pub mod register;
use crate::core::config::Config; use crate::core::build_config::Config;
use crate::core::deps::common::ResolvedDeps; use crate::core::deps::common::ResolvedDeps;
use crate::core::deps::lock::{DepLock, write_lock}; use crate::core::deps::lock::{DepLock, write_lock};
use crate::utils::build::{resolve_pkg_config_flags, run_pkg_config};
use std::path::Path; 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 { fn dep_version(path: &Path) -> String {
Config::open(&path.join("dcr.toml").to_string_lossy()) Config::open(&path.join("dcr.toml").to_string_lossy())
.ok() .ok()
@@ -36,6 +38,18 @@ fn dep_version(path: &Path) -> String {
.unwrap_or_default() .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( pub fn resolve_deps(
config: &Config, config: &Config,
_profile: &str, _profile: &str,
@@ -58,8 +72,24 @@ pub fn resolve_deps(
let mut lock_packages: Vec<DepLock> = Vec::new(); let mut lock_packages: Vec<DepLock> = Vec::new();
if let Some(deps) = deps_table { if let Some(deps) = deps_table {
// Process each dependency declaration in the TOML table
for (name, value) in deps { 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 pkg_info = register::resolve_package_from_registry(name)?;
let dep_root = register::package_root_from_registry_info(&pkg_info)?; let dep_root = register::package_root_from_registry_info(&pkg_info)?;
@@ -74,6 +104,9 @@ pub fn resolve_deps(
.to_string(), .to_string(),
); );
resolved.libs.push(name.clone()); resolved.libs.push(name.clone());
resolved
.package_roots
.push(dep_root.to_string_lossy().to_string());
lock_packages.push(DepLock { lock_packages.push(DepLock {
name: name.clone(), name: name.clone(),
@@ -93,6 +126,8 @@ pub fn resolve_deps(
}); });
} else if let Some(path) = path_dep_path(value) { } else if let Some(path) = path_dep_path(value) {
let dep_root = project_root.join(path); 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(table) = value.as_table() {
if let Some(includes) = table.get("include").and_then(|v| v.as_array()) { if let Some(includes) = table.get("include").and_then(|v| v.as_array()) {
for inc in includes { for inc in includes {
@@ -104,6 +139,7 @@ pub fn resolve_deps(
} }
} else { } else {
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include")); push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
push_package_output_dirs(&mut resolved, &dep_root);
} }
if let Some(lib_dirs) = table.get("lib").and_then(|v| v.as_array()) { if let Some(lib_dirs) = table.get("lib").and_then(|v| v.as_array()) {
@@ -125,12 +161,29 @@ pub fn resolve_deps(
} }
} }
} else { } else {
resolved.libs.push(name.clone()); resolved
.libs
.push(dcr_package_name.clone().unwrap_or_else(|| name.clone()));
} }
} else { } else {
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include")); push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
push_package_output_dirs(&mut resolved, &dep_root);
push_default_lib_dirs(&mut resolved.lib_dirs, &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 { lock_packages.push(DepLock {
@@ -140,16 +193,52 @@ pub fn resolve_deps(
source: format!("path+{}", dep_root.display()), source: format!("path+{}", dep_root.display()),
}); });
} else if let Some(git_info) = git_dep(value) { } 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 { lock_packages.push(DepLock {
name: name.clone(), name: name.clone(),
version: git_info.version.unwrap_or_default(), version: git_info.version.unwrap_or(commit.clone()),
checksum: String::new(), checksum: commit.clone(),
source: format!("git+{}", git_info.url), source: format!("git+{}#{}", git_info.url, commit),
}); });
} }
} }
} }
// Write the lock file with all resolved dependencies
write_lock( write_lock(
project_root, project_root,
&project_name, &project_name,
@@ -160,11 +249,16 @@ pub fn resolve_deps(
Ok(resolved) Ok(resolved)
} }
/// Git dependency specification (url + optional version/ref for the lockfile).
struct GitDep<'a> { struct GitDep<'a> {
url: &'a str, url: &'a str,
version: Option<String>, 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<'_>> { fn git_dep(value: &toml::Value) -> Option<GitDep<'_>> {
if let Some(table) = value.as_table() if let Some(table) = value.as_table()
&& let Some(url) = table.get("git").and_then(|v| v.as_str()) && let Some(url) = table.get("git").and_then(|v| v.as_str())
@@ -173,11 +267,72 @@ fn git_dep(value: &toml::Value) -> Option<GitDep<'_>> {
.get("version") .get("version")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.map(|s| s.to_string()); .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 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> { fn path_dep_path(value: &toml::Value) -> Option<&str> {
if let Some(table) = value.as_table() { if let Some(table) = value.as_table() {
return table.get("path").and_then(|v| v.as_str()); return table.get("path").and_then(|v| v.as_str());
@@ -185,12 +340,14 @@ fn path_dep_path(value: &toml::Value) -> Option<&str> {
register::path_from_string_dep(value) 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) { fn push_if_exists(paths: &mut Vec<String>, path: &Path) {
if path.exists() { if path.exists() {
paths.push(path.to_string_lossy().to_string()); 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) { fn push_default_lib_dirs(paths: &mut Vec<String>, dep_root: &Path) {
for dir in ["lib", "lib64"] { for dir in ["lib", "lib64"] {
push_if_exists(paths, &dep_root.join(dir)); push_if_exists(paths, &dep_root.join(dir));
@@ -198,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")); 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use toml::Value; use toml::Value;
/// Tests that path_dep_path correctly handles both TOML table and legacy string inputs for path dependencies.
#[test] #[test]
fn path_dep_path_supports_table_and_legacy_strings() { fn path_dep_path_supports_table_and_legacy_strings() {
let table = Value::Table( let table = Value::Table(
@@ -225,6 +494,21 @@ mod tests {
assert_eq!(path_dep_path(&Value::String("1.2.3".to_string())), None); 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] #[test]
fn default_lib_dirs_include_packaged_library_output() { fn default_lib_dirs_include_packaged_library_output() {
let root = std::env::temp_dir().join(format!( let root = std::env::temp_dir().join(format!(
@@ -235,10 +519,13 @@ mod tests {
.unwrap() .unwrap()
.as_nanos() .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(); let mut paths = Vec::new();
push_default_lib_dirs(&mut paths, &root); 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); let _ = std::fs::remove_dir_all(root);
} }
} }

View File

@@ -17,35 +17,36 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use sha2::{Digest, Sha256};
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command;
use toml::Value as TomlValue; use toml::Value as TomlValue;
/// Represents a registry configuration entry with its URL and priority.
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Registry { pub struct Registry {
pub url: String, pub url: String,
pub priority: i32, pub priority: i32,
} }
/// Configuration struct for DCR registries, mapping registry names to Registry details.
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct DcrConfig { pub struct DcrConfig {
pub registry: std::collections::HashMap<String, Registry>, pub registry: std::collections::HashMap<String, Registry>,
} }
/// Helper function to get the user's home directory path.
fn home_dir() -> Option<PathBuf> { fn home_dir() -> Option<PathBuf> {
if let Ok(home) = std::env::var("HOME") { crate::utils::fs::home_dir()
return Some(PathBuf::from(home));
}
if let Ok(profile) = std::env::var("USERPROFILE") {
return Some(PathBuf::from(profile));
}
None
} }
/// Helper to get the DCR config directory path.
fn dcr_config_dir() -> Option<PathBuf> { fn dcr_config_dir() -> Option<PathBuf> {
home_dir().map(|h| h.join(".dcr")) 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> { pub fn get_registry_config() -> Option<DcrConfig> {
let home = home_dir()?; let home = home_dir()?;
let config_path = home.join(".dcr/config.toml"); let config_path = home.join(".dcr/config.toml");
@@ -57,6 +58,7 @@ pub fn get_registry_config() -> Option<DcrConfig> {
toml::from_str(&content).ok() 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 { pub fn get_index_path() -> PathBuf {
std::env::var("DCR_INDEX_PATH") std::env::var("DCR_INDEX_PATH")
.map(PathBuf::from) .map(PathBuf::from)
@@ -67,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 { pub fn get_registry_cache_root() -> PathBuf {
let index_path = get_index_path(); let index_path = get_index_path();
if let Some(parent) = index_path.parent() { if let Some(parent) = index_path.parent() {
@@ -82,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> { pub fn package_root_from_registry_info(pkg_info: &JsonValue) -> Result<PathBuf, String> {
let raw_path = pkg_info let raw_path = pkg_info
.get("path") .get("path")
@@ -91,12 +95,7 @@ pub fn package_root_from_registry_info(pkg_info: &JsonValue) -> Result<PathBuf,
return Err("Registry package path is empty".to_string()); return Err("Registry package path is empty".to_string());
} }
let path = Path::new(raw_path); let full_path = registry_path_to_pathbuf(raw_path.trim());
let full_path = if path.is_absolute() {
path.to_path_buf()
} else {
get_registry_cache_root().join(path)
};
if full_path.file_name().and_then(|v| v.to_str()) == Some("dcr.toml") { if full_path.file_name().and_then(|v| v.to_str()) == Some("dcr.toml") {
return full_path return full_path
@@ -108,14 +107,149 @@ pub fn package_root_from_registry_info(pkg_info: &JsonValue) -> Result<PathBuf,
Ok(full_path) 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 { pub fn registry_include_dir(dep_root: &Path) -> PathBuf {
dep_root.join("target").join("include") 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 { pub fn registry_lib_dir(dep_root: &Path) -> PathBuf {
dep_root.join("target").join("lib") 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> { pub fn resolve_package_from_registry(name: &str) -> Result<JsonValue, String> {
let config = get_registry_config().ok_or("No registry config found")?; let config = get_registry_config().ok_or("No registry config found")?;
let mut registries: Vec<(&String, &Registry)> = config.registry.iter().collect(); let mut registries: Vec<(&String, &Registry)> = config.registry.iter().collect();
@@ -143,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 { pub fn is_registry_dep(value: &TomlValue) -> bool {
if let Some(raw) = value.as_str() { if let Some(raw) = value.as_str() {
let raw = raw.trim(); let raw = raw.trim();
@@ -161,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> { pub fn path_from_string_dep(value: &TomlValue) -> Option<&str> {
let raw = value.as_str()?.trim(); let raw = value.as_str()?.trim();
if let Some(path) = raw.strip_prefix("path:") { if let Some(path) = raw.strip_prefix("path:") {
@@ -172,6 +311,7 @@ pub fn path_from_string_dep(value: &TomlValue) -> Option<&str> {
None None
} }
/// Helper to check if a string represents a local file path.
fn is_path_like_string(raw: &str) -> bool { fn is_path_like_string(raw: &str) -> bool {
raw.starts_with("path:") raw.starts_with("path:")
|| raw.starts_with("./") || raw.starts_with("./")
@@ -181,6 +321,7 @@ fn is_path_like_string(raw: &str) -> bool {
|| raw.contains('\\') || raw.contains('\\')
} }
/// Helper to check if a string represents a git or remote dependency.
fn is_git_like_string(raw: &str) -> bool { fn is_git_like_string(raw: &str) -> bool {
raw.starts_with("git:") raw.starts_with("git:")
|| raw.starts_with("github:") || raw.starts_with("github:")
@@ -225,16 +366,44 @@ mod tests {
#[test] #[test]
fn registry_package_root_accepts_package_dir_or_manifest_path() { 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!( assert_eq!(
package_root_from_registry_info(&pkg).unwrap(), package_root_from_registry_info(&pkg)
PathBuf::from("/tmp/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!( assert_eq!(
package_root_from_registry_info(&pkg).unwrap(), package_root_from_registry_info(&pkg)
PathBuf::from("/tmp/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,8 +15,9 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod builder; /// Core module for DCR, a Cargo-like C/C++ project manager.
pub mod config; pub mod build;
pub mod build_config;
pub mod deps; pub mod deps;
pub mod registry; pub mod registry;
pub mod runner; pub mod runner;

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