mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 11:38:30 +00:00
Compare commits
50 Commits
f29322a547
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f47e81610 | |||
| d49b271e6a | |||
| f17e4c6642 | |||
| a890f7e6eb | |||
|
|
3b6251ec33 | ||
|
|
e9d09eacaf | ||
|
|
2f4cde9177 | ||
| 552f6c8e4f | |||
|
|
da84916e10 | ||
| cb6ae3dcc7 | |||
|
|
10123ddedb | ||
|
|
0a140c75d8 | ||
|
|
aa1d4ba807 | ||
|
|
991d1c20af | ||
|
|
df674defe1 | ||
| 7d2340fb52 | |||
| 6c79340736 | |||
| be14cfb8e2 | |||
| 3305915c07 | |||
| 5cb51878a5 | |||
| d587eba5a4 | |||
| bfd2b9d251 | |||
| 1e3eb658dc | |||
| f5dad74498 | |||
| 3c25539c11 | |||
| 54653dfd80 | |||
| f3605a86a2 | |||
| 9d21294cb4 | |||
| 8609d12179 | |||
| b6ecb2aa79 | |||
| dd89109f92 | |||
| c6af6c5196 | |||
| b6153dbaac | |||
| 9e2b7a897a | |||
| 9add069879 | |||
| 5eb00037ea | |||
| f7353f9d03 | |||
| 05e49838f7 | |||
| b0e426c189 | |||
| 60d7420929 | |||
| 3bd91e85f9 | |||
| a20c61cc88 | |||
| bc308d6996 | |||
| 09e7f5b880 | |||
| 3f925f437f | |||
| b9cbef43e9 | |||
| 9bf1346d1b | |||
| 794d571d58 | |||
| d2df85a064 | |||
| 5e8037da97 |
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -6,7 +6,19 @@ on:
|
||||
- master
|
||||
- main
|
||||
- dev
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'tests/**'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- '.github/workflows/ci.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'tests/**'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- '.github/workflows/ci.yml'
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -151,4 +163,4 @@ jobs:
|
||||
|
||||
- name: sccache stats
|
||||
if: always()
|
||||
run: sccache --show-stats || true
|
||||
run: sccache --show-stats || true
|
||||
38
.github/workflows/code-mirror.yml
vendored
Normal file
38
.github/workflows/code-mirror.yml
vendored
Normal 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
290
.github/workflows/release-mirror.yml
vendored
Normal 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
|
||||
93
.github/workflows/release.yml
vendored
93
.github/workflows/release.yml
vendored
@@ -45,6 +45,10 @@ on:
|
||||
description: "Update Homebrew tap"
|
||||
type: boolean
|
||||
default: false
|
||||
run_s3:
|
||||
description: "Mirror Release to Yandex Object Storage (S3)"
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -1187,6 +1191,7 @@ jobs:
|
||||
- name: Update Formula/dcr.rb
|
||||
run: |
|
||||
VERSION="${{ needs.setup.outputs.version }}"
|
||||
|
||||
cat > Formula/dcr.rb <<FORMULA
|
||||
class Dcr < Formula
|
||||
desc "Cargo-like utility to manage C/C++ projects"
|
||||
@@ -1195,37 +1200,36 @@ jobs:
|
||||
|
||||
on_macos do
|
||||
on_intel do
|
||||
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-x86_64-apple-darwin-#{version}"
|
||||
url "https://github.com/dexoron/dcr/releases/download/v${VERSION}/dcr-x86_64-apple-darwin-${VERSION}"
|
||||
sha256 "${{ steps.sha.outputs.macos_x86 }}"
|
||||
end
|
||||
|
||||
on_arm do
|
||||
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-aarch64-apple-darwin-#{version}"
|
||||
url "https://github.com/dexoron/dcr/releases/download/v${VERSION}/dcr-aarch64-apple-darwin-${VERSION}"
|
||||
sha256 "${{ steps.sha.outputs.macos_arm }}"
|
||||
end
|
||||
end
|
||||
|
||||
on_linux do
|
||||
on_intel do
|
||||
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-x86_64-unknown-linux-gnu-#{version}"
|
||||
url "https://github.com/dexoron/dcr/releases/download/v${VERSION}/dcr-x86_64-unknown-linux-gnu-${VERSION}"
|
||||
sha256 "${{ steps.sha.outputs.linux_x86 }}"
|
||||
end
|
||||
|
||||
on_arm do
|
||||
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-aarch64-unknown-linux-gnu-#{version}"
|
||||
url "https://github.com/dexoron/dcr/releases/download/v${VERSION}/dcr-aarch64-unknown-linux-gnu-${VERSION}"
|
||||
sha256 "${{ steps.sha.outputs.linux_arm }}"
|
||||
end
|
||||
end
|
||||
|
||||
def install
|
||||
bin.install Dir["dcr*"].first => "dcr"
|
||||
end
|
||||
|
||||
resource "man-pages" do
|
||||
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-man-pages-#{version}.tar.gz"
|
||||
url "https://github.com/dexoron/dcr/releases/download/v${VERSION}/dcr-man-pages-${VERSION}.tar.gz"
|
||||
sha256 "${{ steps.sha.outputs.man_pages }}"
|
||||
end
|
||||
|
||||
def install
|
||||
bin.install Dir["dcr*"].first => "dcr"
|
||||
|
||||
resource("man-pages").stage do
|
||||
man1.install Dir["usr/share/man/man1/*.1"]
|
||||
end
|
||||
@@ -1253,4 +1257,73 @@ jobs:
|
||||
# sudo snap install --classic --dangerous dcrup_*.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
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -7,4 +7,6 @@ sandbox/**
|
||||
|
||||
personal/
|
||||
.*
|
||||
!.gitignore
|
||||
!.gitignore
|
||||
!.github/
|
||||
graphify-out/
|
||||
229
CHANGELOG.md
229
CHANGELOG.md
@@ -1,5 +1,199 @@
|
||||
# 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
|
||||
@@ -215,25 +409,31 @@
|
||||
### RU
|
||||
|
||||
**Добавлено:**
|
||||
|
||||
- Учтён `target_env` в `default_target_triple()` — Linux (gnu/musl) и Windows (msvc/gnu) теперь используют правильное окружение, а не хардкод.
|
||||
|
||||
**Исправлено:**
|
||||
|
||||
- **Сборка на macOS Apple Silicon падала с `_main` undefined** — `--target=` теперь передаётся и в ldflags, не только в cflags. `default_target_triple()` на macOS использует `std::env::consts::ARCH` вместо хардкода `x86_64`.
|
||||
- **Бейджи GitHub Stars и GPL-3.0 не рендерились в README** — блок бейджей переведён на чистый HTML.
|
||||
|
||||
**Изменено:**
|
||||
|
||||
- Вычисление таргета по умолчанию вынесено в общую функцию `default_target_triple()` — устранено дублирование в build, run, clean.
|
||||
|
||||
### EN
|
||||
|
||||
**Added:**
|
||||
|
||||
- `default_target_triple()` now respects `target_env` — Linux (gnu/musl) and Windows (msvc/gnu) use the correct environment instead of hardcoded values.
|
||||
|
||||
**Fixed:**
|
||||
|
||||
- **macOS Apple Silicon builds failed with `_main` undefined** — `--target=` is now passed to ldflags, not just cflags. `default_target_triple()` uses `std::env::consts::ARCH` on macOS instead of hardcoded `x86_64`.
|
||||
- **GitHub Stars and GPL-3.0 badges not rendering in README** — badge block converted to pure HTML.
|
||||
|
||||
**Changed:**
|
||||
|
||||
- Default target resolution extracted into shared `default_target_triple()` — removed duplication across build, run, clean.
|
||||
|
||||
## [0.7.1] - 2026-06-02 "Мультиархитектурное расширение / The Multi-Arch Expansion"
|
||||
@@ -241,6 +441,7 @@
|
||||
### RU
|
||||
|
||||
**Добавлено:**
|
||||
|
||||
- Вывод `--help` для всех команд с цветным форматированием.
|
||||
- man-страницы (12 штук в `man/man1/`) и их установка во всех пакетах.
|
||||
- Валидация имен проектов в `dcr init` и `dcr new` (только ASCII буквы, цифры, `_` и `-`).
|
||||
@@ -251,6 +452,7 @@
|
||||
- Пакеты AUR (`dcr-dev`, `dcr-dev-bin`) и сборка Snap в CI.
|
||||
|
||||
**Изменено:**
|
||||
|
||||
- Функция `validate_package_name` стала публичной.
|
||||
- Команды `gen`, `tree`, `fmt`, `setup` больше не игнорируют аргументы и используют цветной вывод.
|
||||
- Инструкция в README изменена с `| bash` на `| sh` для POSIX.
|
||||
@@ -268,6 +470,7 @@
|
||||
### EN
|
||||
|
||||
**Added:**
|
||||
|
||||
- `--help` for all commands with styled headers and usage lines.
|
||||
- Man pages (12 troff man pages in `man/man1/`) and their installation across all packaging formats.
|
||||
- Project name validation in `dcr init` and `dcr new` (ASCII letters, digits, `_`, `-` only).
|
||||
@@ -278,6 +481,7 @@
|
||||
- AUR packages (`dcr-dev`, `dcr-dev-bin`) and Snap build in CI.
|
||||
|
||||
**Changed:**
|
||||
|
||||
- `validate_package_name` made public and callable from CLI commands directly.
|
||||
- `gen.rs`, `tree.rs`, `fmt.rs`, `setup.rs` no longer ignore arguments and use styled output.
|
||||
- README install instructions changed from `| bash` to `| sh` for POSIX compatibility.
|
||||
@@ -454,6 +658,7 @@ Added:
|
||||
```
|
||||
|
||||
Produces `KERNEL.EFI` (works for `bin`, `staticlib`, and `sharedlib`).
|
||||
|
||||
- Automatic injection of `--target=<build.target>` into compiler flags when `build.target` is set in `dcr.toml`. This greatly simplifies clang-based cross-compilation (especially for bare-metal targets like `aarch64-none-elf`).
|
||||
- Bare-metal targets (containing `none`, `-elf`, `eabi`, `baremetal`) no longer receive DCR's internal default flags (`-g`, `-Wall`, `-Wextra`, `-fno-omit-frame-pointer`, `-DDCR_DEBUG`, etc.). Prevents unwanted sections (`.comment`, debug info, etc.) that break custom linker scripts when `inherit = true`.
|
||||
- `build.ldscript` in `dcr.toml` — linker script path passed as `-T <path>` to the linker. Essential for bare-metal/embedded/freestanding targets.
|
||||
@@ -488,28 +693,28 @@ Fixed:
|
||||
|
||||
- Path dependencies now work correctly: `dcr add` stores them as `{ path =
|
||||
"./..." }` tables, `is_registry_dep` properly distinguishes registry strings
|
||||
from path/git strings, and `deps/mod.rs` resolves both table-form and
|
||||
legacy string-form path dependencies
|
||||
from path/git strings, and `deps/mod.rs` resolves both table-form and
|
||||
legacy string-form path dependencies
|
||||
- Registry dependency paths no longer hardcoded to `project_root/dcr-index`;
|
||||
now use `package_root_from_registry_info()` which resolves relative to the
|
||||
registry cache root (`~/.dcr/`)
|
||||
now use `package_root_from_registry_info()` which resolves relative to the
|
||||
registry cache root (`~/.dcr/`)
|
||||
- Registry dependencies are now actually built: `build_project_at()` is
|
||||
called when `include_dir` or `lib_dir` is missing
|
||||
called when `include_dir` or `lib_dir` is missing
|
||||
|
||||
Added:
|
||||
|
||||
- `SIGINT`/`Ctrl+C` handler via `ctrlc` crate — `dcr build` now checks
|
||||
`BUILD_INTERRUPTED` flag at key points and aborts cleanly
|
||||
`BUILD_INTERRUPTED` flag at key points and aborts cleanly
|
||||
- `utils/build.rs` — extracted shared utilities: `parse_version_info`,
|
||||
`normalize_target_os`, `resolve_compiler`, `primary_language`,
|
||||
`resolve_pkg_config_flags` and config helpers. Eliminates code duplication
|
||||
between `cli/build.rs`, `cli/run.rs`, `cli/clean.rs`, `cli/gen.rs`
|
||||
`normalize_target_os`, `resolve_compiler`, `primary_language`,
|
||||
`resolve_pkg_config_flags` and config helpers. Eliminates code duplication
|
||||
between `cli/build.rs`, `cli/run.rs`, `cli/clean.rs`, `cli/gen.rs`
|
||||
- `utils/fs.rs::with_dir` — extracted common directory-scoped execution
|
||||
- `run_command_sync_output` in `builder/common.rs` with global `OUTPUT_MUTEX`
|
||||
— synchronized compiler output across all backends (unix_cc, gas, nasm, msvc)
|
||||
— synchronized compiler output across all backends (unix_cc, gas, nasm, msvc)
|
||||
- New helper functions in `deps/register.rs`: `get_registry_cache_root`,
|
||||
`package_root_from_registry_info`, `registry_include_dir`, `registry_lib_dir`,
|
||||
`path_from_string_dep`
|
||||
`package_root_from_registry_info`, `registry_include_dir`, `registry_lib_dir`,
|
||||
`path_from_string_dep`
|
||||
- Unit tests for `register.rs` (`is_registry_dep`, `package_root_from_registry_info`),
|
||||
`deps/mod.rs` (`path_dep_path`, `push_default_lib_dirs`),
|
||||
`utils/build.rs` (`normalize_target_os`, `parse_version_info`)
|
||||
|
||||
@@ -11,6 +11,7 @@ Thanks for your interest in the project.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dexoron/dcr.git
|
||||
# or a source mirror: GitVerse / SourceCraft / SourceForge / GitLab (see README)
|
||||
cd dcr
|
||||
cargo check
|
||||
```
|
||||
|
||||
287
Cargo.lock
generated
287
Cargo.lock
generated
@@ -2,6 +2,15 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
@@ -11,6 +20,56 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
@@ -106,6 +165,12 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@@ -154,11 +219,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dcr"
|
||||
version = "0.8.2"
|
||||
version = "0.9.1"
|
||||
dependencies = [
|
||||
"ctrlc",
|
||||
"env_logger",
|
||||
"fatfs",
|
||||
"glob",
|
||||
"log",
|
||||
"owo-colors",
|
||||
"self-replace",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -168,6 +236,37 @@ dependencies = [
|
||||
"ureq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"defmt-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt-macros"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
|
||||
dependencies = [
|
||||
"defmt-parser",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt-parser"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -198,7 +297,30 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"env_filter",
|
||||
"jiff",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -475,12 +597,54 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
|
||||
dependencies = [
|
||||
"defmt",
|
||||
"jiff-core",
|
||||
"jiff-static",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-core"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
|
||||
dependencies = [
|
||||
"defmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204"
|
||||
dependencies = [
|
||||
"jiff-core",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.103"
|
||||
@@ -518,9 +682,9 @@ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.30"
|
||||
version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
@@ -570,12 +734,24 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "4.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
@@ -588,6 +764,21 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.5"
|
||||
@@ -604,7 +795,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -631,6 +822,35 @@ version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
@@ -757,7 +977,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -828,6 +1048,17 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.2"
|
||||
@@ -836,7 +1067,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -852,6 +1083,26 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.3"
|
||||
@@ -976,6 +1227,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
@@ -1032,7 +1289,7 @@ dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -1100,7 +1357,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1111,7 +1368,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1271,7 +1528,7 @@ dependencies = [
|
||||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
@@ -1287,7 +1544,7 @@ dependencies = [
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
@@ -1354,7 +1611,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -1375,7 +1632,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -1415,7 +1672,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
18
Cargo.toml
18
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dcr"
|
||||
version = "0.8.2"
|
||||
version = "0.9.1"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
|
||||
@@ -75,6 +75,11 @@ serde_json = "1"
|
||||
ctrlc = "3"
|
||||
fatfs = { version = "0.3", optional = true }
|
||||
|
||||
# Logging
|
||||
log = "0.4.34"
|
||||
env_logger = "0.11.11"
|
||||
owo-colors = "4"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = "thin"
|
||||
@@ -88,4 +93,13 @@ opt-level = 1
|
||||
lto = false
|
||||
codegen-units = 16
|
||||
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
11
FAQ.md
@@ -61,8 +61,19 @@ Available installation methods may vary by project version.
|
||||
Recommended path:
|
||||
|
||||
- check instructions in `README.md`;
|
||||
- use `dcrup install stable` (default mirror chain: **cf → gh → sf → ya**);
|
||||
- pin a host with `--mirror cf|gh|sf|ya` or `DCRUP_MIRROR` if needed;
|
||||
- if there is no prebuilt package for your platform, build from source using `cargo build --release`.
|
||||
|
||||
## How do I choose a download mirror?
|
||||
|
||||
`dcrup` defaults to `--mirror auto`: CloudFlare CDN, then GitHub, then SourceForge, then Yandex CDN.
|
||||
|
||||
```sh
|
||||
dcrup install stable --mirror cf
|
||||
export DCRUP_MIRROR=ya
|
||||
```
|
||||
|
||||
## How can I get started quickly?
|
||||
|
||||
Minimal workflow:
|
||||
|
||||
46
README.md
46
README.md
@@ -39,7 +39,10 @@
|
||||
|
||||
```sh
|
||||
yay -S dcr
|
||||
# or
|
||||
paru -S dcr
|
||||
```
|
||||
> 💡 Do you like DCR? Vote for the package on the [AUR](https://aur.archlinux.org/packages/dcr) page—it will help the project get into the official "extra" repository!
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
@@ -83,7 +86,9 @@ cargo install dcr
|
||||
```sh
|
||||
curl -fsSL https://sh.dcr-tool.ru | sh -s -- self-install
|
||||
export PATH="$HOME/.dcr/bin:$PATH"
|
||||
dcrup install stable
|
||||
dcrup install stable # auto: cf → gh → sf → ya
|
||||
# dcrup install stable --mirror cf # CloudFlare CDN
|
||||
# dcrup install stable --mirror ya # Yandex CDN (RU)
|
||||
```
|
||||
|
||||
</td>
|
||||
@@ -98,7 +103,9 @@ irm https://ps1.dcr-tool.ru | iex
|
||||
# irm https://ps1.dcr-tool.ru -OutFile dcrup.ps1
|
||||
# powershell -File .\dcrup.ps1 self-install
|
||||
# $env:Path += ";$env:USERPROFILE\.dcr\bin"
|
||||
dcrup install stable
|
||||
dcrup install stable # auto: cf → gh → sf → ya
|
||||
# dcrup install stable --mirror cf # CloudFlare CDN
|
||||
# dcrup install stable --mirror ya # Yandex CDN (RU)
|
||||
```
|
||||
|
||||
</td>
|
||||
@@ -216,6 +223,41 @@ Full docs at **[dcr.dexoron.su](https://dcr.dexoron.su)** or in the [`docs/`](do
|
||||
|
||||
---
|
||||
|
||||
## Repository Mirrors
|
||||
|
||||
The primary repository is hosted on GitHub. Source and release mirrors:
|
||||
|
||||
| Region | Mirror | Contents |
|
||||
|---|---|---|
|
||||
| Worldwide | [GitHub](https://github.com/dexoron/dcr) | Source code and releases |
|
||||
| Worldwide | [SourceForge](https://sourceforge.net/projects/dcr-tool/) | Source code and releases |
|
||||
| Russia | [GitVerse](https://gitverse.ru/dcr_labs/dcr) | Source code |
|
||||
| Russia | [SourceCraft](https://sourcecraft.dev/dexoron-dev/dcr) | Source code |
|
||||
|
||||
Release CDN (used by `dcrup`):
|
||||
|
||||
| Code | Host | Role |
|
||||
|---|---|---|
|
||||
| `cf` | [cdn-global.dcr-tool.ru](https://cdn-global.dcr-tool.ru) (CloudFlare) | Worldwide CDN |
|
||||
| `gh` | GitHub Releases | Primary host |
|
||||
| `sf` | SourceForge | Worldwide fallback |
|
||||
| `ya` | [cdn-ru.dcr-tool.ru](https://cdn-ru.dcr-tool.ru) (Yandex) | Paid RU fallback for restrictions |
|
||||
|
||||
`dcrup install` defaults to `--mirror auto` and tries **cf → gh → sf → ya**. Override with `--mirror cf|gh|sf|ya` or `DCRUP_MIRROR`.
|
||||
|
||||
---
|
||||
|
||||
## VS Code Extension
|
||||
|
||||
Install the recommended [DCR extension by Dexoron-Labs](https://marketplace.visualstudio.com/items?itemName=Dexoron-Labs.dcr) for VS Code. Do not install Microsoft C/C++ extensions for DCR projects; DCR uses `clangd` instead. If you need debugging with Microsoft's debugger, use MS C/C++ only for its debugger component.
|
||||
|
||||
Recommended companion extensions:
|
||||
|
||||
- [Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml)
|
||||
- [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb), or MS C/C++ for debugger support only
|
||||
|
||||
---
|
||||
|
||||
## Contributors
|
||||
|
||||
<div align="center">
|
||||
|
||||
13
dcrup.cmd
13
dcrup.cmd
@@ -1,13 +0,0 @@
|
||||
@echo off
|
||||
setlocal
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
if exist "%SCRIPT_DIR%dcrup.ps1" (
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%dcrup.ps1" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
)
|
||||
if exist "%USERPROFILE%\.dcr\bin\dcrup.ps1" (
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%USERPROFILE%\.dcr\bin\dcrup.ps1" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
)
|
||||
echo [error] dcrup.ps1 not found near dcrup.cmd >&2
|
||||
exit /b 1
|
||||
138
dcrup.ps1
138
dcrup.ps1
@@ -1,4 +1,4 @@
|
||||
#Requires -Version 5.1
|
||||
#Requires -Version 5.1
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$DcrupHome = if ($env:DCRUP_HOME) { $env:DCRUP_HOME } else { Join-Path $env:USERPROFILE '.dcr' }
|
||||
@@ -6,6 +6,8 @@ $DcrupBin = if ($env:DCRUP_BIN) { $env:DCRUP_BIN } else { Join-Path $DcrupHom
|
||||
$DcrupTc = Join-Path $DcrupHome 'toolchains'
|
||||
$DcrupMeta = Join-Path $DcrupHome 'meta'
|
||||
$RepoUrl = if ($env:DCRUP_REPO) { $env:DCRUP_REPO } else { 'https://github.com/dexoron/dcr' }
|
||||
$InstallUrl = if ($env:DCRUP_INSTALL_URL) { $env:DCRUP_INSTALL_URL } else { 'https://ps1.dcr-tool.ru' }
|
||||
$DcrupMirror = if ($env:DCRUP_MIRROR) { $env:DCRUP_MIRROR } else { 'auto' }
|
||||
$ApiLatest = 'https://api.github.com/repos/dexoron/dcr/releases/latest'
|
||||
$ApiAll = 'https://api.github.com/repos/dexoron/dcr/releases'
|
||||
$Features = if ($env:DCR_FEATURES) { $env:DCR_FEATURES } else { 'archive' }
|
||||
@@ -20,7 +22,7 @@ function Show-Usage {
|
||||
dcrup — install and switch DCR versions (Windows)
|
||||
|
||||
Usage:
|
||||
dcrup install <spec> [--build|--release] [--force]
|
||||
dcrup install <spec> [--build|--release] [--force] [--mirror auto|cf|gh|sf|ya]
|
||||
dcrup default <spec>
|
||||
dcrup update
|
||||
dcrup list
|
||||
@@ -38,15 +40,24 @@ Spec:
|
||||
night → always build from branch "dev" HEAD
|
||||
|
||||
Modes:
|
||||
--release download GitHub Release binary (default for stable/dev)
|
||||
--release download prebuilt binary (default for stable/dev)
|
||||
--build cargo build --release --features archive
|
||||
night always --build (prebuilt not available)
|
||||
|
||||
Mirrors (prebuilt download only):
|
||||
--mirror auto try cf → gh → sf → ya, first successful download wins (default)
|
||||
--mirror cf download from cdn-global.dcr-tool.ru
|
||||
--mirror gh download from github.com/dexoron/dcr
|
||||
--mirror sf download from sourceforge.net/projects/dcr-tool
|
||||
--mirror ya download from cdn-ru.dcr-tool.ru
|
||||
DCRUP_MIRROR env override (default: auto)
|
||||
|
||||
Env:
|
||||
DCRUP_HOME default ~/.dcr
|
||||
DCRUP_BIN default $DCRUP_HOME/bin
|
||||
DCR_FEATURES default archive
|
||||
DCRUP_REPO git/GitHub repo URL
|
||||
DCRUP_MIRROR auto|cf|gh|sf|ya (default: auto)
|
||||
'@ | Write-Host
|
||||
}
|
||||
|
||||
@@ -123,6 +134,65 @@ function Link-Default([string]$id) {
|
||||
Ok "default → $id ($dst)"
|
||||
}
|
||||
|
||||
function Resolve-InstalledId([string]$spec) {
|
||||
Parse-Spec $spec
|
||||
if ($script:SpecChannel -eq 'night' -or $script:SpecFloating) {
|
||||
$item = Get-ChildItem $DcrupTc -Directory -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -like "$($script:SpecChannel)-*" } |
|
||||
Sort-Object Name | Select-Object -Last 1
|
||||
if ($item) { return $item.Name }
|
||||
return $null
|
||||
}
|
||||
return (Toolchain-Id)
|
||||
}
|
||||
|
||||
function Set-Mirror([string]$value) {
|
||||
if ($value -notin @('auto', 'cf', 'gh', 'sf', 'ya')) {
|
||||
Die "invalid --mirror: $value (use auto|cf|gh|sf|ya)"
|
||||
}
|
||||
$script:DcrupMirror = $value
|
||||
}
|
||||
|
||||
function Get-MirrorUrl([string]$mirror, [string]$tag, [string]$asset) {
|
||||
switch ($mirror) {
|
||||
'cf' { return "https://cdn-global.dcr-tool.ru/$tag/$asset" }
|
||||
'gh' { return "https://github.com/dexoron/dcr/releases/download/$tag/$asset" }
|
||||
'sf' { return "https://sourceforge.net/projects/dcr-tool/files/$tag/$asset/download" }
|
||||
'ya' { return "https://cdn-ru.dcr-tool.ru/$tag/$asset" }
|
||||
default { Die "invalid mirror: $mirror" }
|
||||
}
|
||||
}
|
||||
|
||||
function Get-MirrorsToTry {
|
||||
switch ($script:DcrupMirror) {
|
||||
'auto' { return @('cf', 'gh', 'sf', 'ya') }
|
||||
{ $_ -in @('cf', 'gh', 'sf', 'ya') } { return @($script:DcrupMirror) }
|
||||
default { Die "invalid mirror: $($script:DcrupMirror) (use auto|cf|gh|sf|ya)" }
|
||||
}
|
||||
}
|
||||
|
||||
function Download-FromMirrors([string]$tag, [string]$asset, [string]$destFile) {
|
||||
$tmp = "$destFile.download"
|
||||
foreach ($m in (Get-MirrorsToTry)) {
|
||||
$url = Get-MirrorUrl $m $tag $asset
|
||||
Info "trying mirror $m …"
|
||||
Remove-Item $tmp -ErrorAction SilentlyContinue
|
||||
try {
|
||||
Invoke-WebRequest -Uri $url -OutFile $tmp -UserAgent 'dcrup'
|
||||
$size = (Get-Item $tmp).Length
|
||||
if ($size -gt 100000) {
|
||||
Move-Item $tmp $destFile -Force
|
||||
return $m
|
||||
}
|
||||
Warn "mirror $m returned a too-small file ($size bytes)"
|
||||
} catch {
|
||||
Warn "mirror $m failed"
|
||||
}
|
||||
Remove-Item $tmp -ErrorAction SilentlyContinue
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Fetch-Release($channel, $version) {
|
||||
if ($version) {
|
||||
return Invoke-RestMethod -Uri "https://api.github.com/repos/dexoron/dcr/releases/tags/v$version"
|
||||
@@ -142,8 +212,6 @@ function Install-Prebuilt([switch]$Force) {
|
||||
$tag = $rel.tag_name
|
||||
$ver = Normalize-Version $tag
|
||||
$assetName = "dcr-$triple-$ver.exe"
|
||||
$asset = $rel.assets | Where-Object { $_.name -eq $assetName } | Select-Object -First 1
|
||||
if (-not $asset) { Die "asset not found: $assetName" }
|
||||
|
||||
$id = if ($script:SpecFloating) { Toolchain-Id $ver } else { Toolchain-Id }
|
||||
$dest = Join-Path $DcrupTc $id
|
||||
@@ -155,16 +223,19 @@ function Install-Prebuilt([switch]$Force) {
|
||||
}
|
||||
Ensure-Dirs
|
||||
New-Item -ItemType Directory -Force -Path $dest | Out-Null
|
||||
Info "downloading $assetName …"
|
||||
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $bin
|
||||
Info "downloading $assetName (mirror=$($script:DcrupMirror)) …"
|
||||
$mirror = Download-FromMirrors $tag $assetName $bin
|
||||
if (-not $mirror) { Die "no mirror reachable for $assetName (try: --mirror cf|gh|sf|ya)" }
|
||||
Info "mirror → $mirror"
|
||||
@"
|
||||
channel=$($script:SpecChannel)
|
||||
version=$ver
|
||||
tag=$tag
|
||||
source=release
|
||||
triple=$triple
|
||||
mirror=$mirror
|
||||
"@ | Set-Content (Join-Path $dest 'dcrup-meta')
|
||||
Ok "installed prebuilt $id"
|
||||
Ok "installed prebuilt $id ($triple via $mirror)"
|
||||
Link-Default $id
|
||||
}
|
||||
|
||||
@@ -270,23 +341,32 @@ function Cmd-Install {
|
||||
$mode = 'release'
|
||||
$force = $false
|
||||
$spec = $null
|
||||
foreach ($a in $Args) {
|
||||
$i = 0
|
||||
while ($i -lt $Args.Count) {
|
||||
$a = $Args[$i]
|
||||
switch -Regex ($a) {
|
||||
'^--build$' { $mode = 'build'; continue }
|
||||
'^--release$' { $mode = 'release'; continue }
|
||||
'^--force$' { $force = $true; continue }
|
||||
'^--build$' { $mode = 'build' }
|
||||
'^--release$' { $mode = 'release' }
|
||||
'^--force$' { $force = $true }
|
||||
'^--mirror$' {
|
||||
if ($i + 1 -ge $Args.Count) { Die '--mirror needs auto|cf|gh|sf|ya' }
|
||||
$i++
|
||||
Set-Mirror $Args[$i]
|
||||
}
|
||||
'^--mirror=(.+)$' { Set-Mirror $Matches[1] }
|
||||
'^-' { Die "unknown flag: $a" }
|
||||
default {
|
||||
if ($spec) { Die "unexpected: $a" }
|
||||
$spec = $a
|
||||
}
|
||||
}
|
||||
$i++
|
||||
}
|
||||
if (-not $spec) { Die 'usage: dcrup install <spec> [--build|--release] [--force]' }
|
||||
if (-not $spec) { Die 'usage: dcrup install <spec> [--build|--release] [--force] [--mirror auto|cf|gh|sf|ya]' }
|
||||
Parse-Spec $spec
|
||||
Ensure-Dirs
|
||||
if ($script:SpecChannel -eq 'night') { $mode = 'build' }
|
||||
Info "install spec=$spec channel=$($script:SpecChannel) mode=$mode"
|
||||
Info "install spec=$spec channel=$($script:SpecChannel) mode=$mode mirror=$($script:DcrupMirror)"
|
||||
if ($mode -eq 'build') { Install-Build -Force:$force } else { Install-Prebuilt -Force:$force }
|
||||
Ensure-UserPath
|
||||
Install-CmdShim
|
||||
@@ -312,7 +392,19 @@ function Cmd-Show {
|
||||
|
||||
function Cmd-SelfInstall {
|
||||
Ensure-Dirs
|
||||
Copy-Item $PSCommandPath (Join-Path $DcrupBin 'dcrup.ps1') -Force
|
||||
if ([string]::IsNullOrWhiteSpace($PSCommandPath) -or -not (Test-Path $PSCommandPath)) {
|
||||
# With `irm URL | iex`, PowerShell evaluates text and there is no
|
||||
# source path. Download the canonical installer for self-install.
|
||||
$tmp = Join-Path $env:TEMP ("dcrup-" + [guid]::NewGuid().ToString('N') + '.ps1')
|
||||
try {
|
||||
Invoke-WebRequest -Uri $InstallUrl -OutFile $tmp -UseBasicParsing -UserAgent 'dcrup'
|
||||
Copy-Item $tmp (Join-Path $DcrupBin 'dcrup.ps1') -Force
|
||||
} finally {
|
||||
Remove-Item $tmp -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
} else {
|
||||
Copy-Item $PSCommandPath (Join-Path $DcrupBin 'dcrup.ps1') -Force
|
||||
}
|
||||
Install-CmdShim
|
||||
Ensure-UserPath
|
||||
Ok "dcrup installed to $DcrupBin (use: dcrup …)"
|
||||
@@ -350,7 +442,7 @@ switch ($cmd) {
|
||||
if ($a -like 'night-*') { Cmd-Install @('night', '--build', '--force') }
|
||||
elseif ($a -like 'stable-*') { Cmd-Install @('stable', '--force') }
|
||||
elseif ($a -like 'dev-*') { Cmd-Install @('dev', '--force') }
|
||||
else { Ok "pinned $a — install a newer version explicitly" }
|
||||
else { Ok "pinned $a - install a newer version explicitly" }
|
||||
}
|
||||
'uninstall' {
|
||||
if ($rest -contains '--all') {
|
||||
@@ -359,9 +451,21 @@ switch ($cmd) {
|
||||
Remove-Item (Active-Path) -ErrorAction SilentlyContinue
|
||||
Ok 'removed all'
|
||||
} else {
|
||||
Die 'use: dcrup uninstall --all (or install a replacement first)'
|
||||
if ($rest.Count -ne 1) { Die 'usage: dcrup uninstall <spec>|--all' }
|
||||
$id = Resolve-InstalledId $rest[0]
|
||||
if (-not $id) { Die "not installed: $($rest[0])" }
|
||||
$dir = Join-Path $DcrupTc $id
|
||||
if (-not (Test-Path $dir)) { Die "not installed: $($rest[0])" }
|
||||
Remove-Item -Recurse -Force $dir
|
||||
if ((Read-Active) -eq $id) {
|
||||
Remove-Item (Join-Path $DcrupBin 'dcr.exe') -ErrorAction SilentlyContinue
|
||||
Remove-Item (Active-Path) -ErrorAction SilentlyContinue
|
||||
Warn 'removed active toolchain; install or select another default'
|
||||
}
|
||||
Ok "removed $id"
|
||||
}
|
||||
}
|
||||
'self-install' { Cmd-SelfInstall }
|
||||
default { Die "unknown command: $cmd" }
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ sidebar_label: Introduction
|
||||
# Installation (dcrup)
|
||||
curl -fsSL https://sh.dcr-tool.ru | sh -s -- self-install
|
||||
export PATH="$HOME/.dcr/bin:$PATH"
|
||||
dcrup install stable
|
||||
dcrup install stable # auto: cf → gh → sf → ya
|
||||
# dcrup install stable --mirror cf # CloudFlare / GitHub / SourceForge / Yandex
|
||||
|
||||
# Create a project
|
||||
dcr new my-app
|
||||
|
||||
@@ -4,6 +4,85 @@ sidebar_label: Changelog
|
||||
|
||||
# Changelog
|
||||
|
||||
## 0.9.1 (2026-09-13)
|
||||
|
||||
- **`pkg-config` dependencies** — declare `fmt = { pkg-config = "fmt" }` or multiple package names in `[dependencies]`; DCR applies `.pc` compiler/linker flags and records the resolved package in `dcr.lock`.
|
||||
- **`dcr add` support for `pkg-config`** — easily add system package dependencies with `dcr add fmt pkg-config:fmt` or `dcr add openssl pkg:openssl,libcrypto`.
|
||||
- **`dcr tree` & IDE integration** — `pkg-config` packages are marked in `dcr tree` and resolved into compiler flags in `dcr gen` (`compile_commands.json`).
|
||||
- **Fixed `dcr new` in non-ASCII / spaced paths** — initializing projects from folders with non-ASCII characters or spaces no longer fails validation.
|
||||
|
||||
## 0.9.0 (2026-08-31)
|
||||
|
||||
- **Download mirrors in `dcrup`** — `--mirror auto|cf|gh|sf|ya` and `DCRUP_MIRROR`.
|
||||
Default `auto` tries **cf → gh → sf → ya** (CloudFlare CDN, GitHub, SourceForge, Yandex CDN).
|
||||
- **Complete Path/Git dependencies** — DCR packages from `path` and `git` are built before their consumer. Git sources are cached in `~/.dcr/git`, support `branch`/`tag`/`rev`, and record the exact commit in `dcr.lock`.
|
||||
- **Build and prebuild modes** — `path` and `git` dependencies can build a DCR package from `dcr.toml` or link existing `include`/`lib`/`libs` artifacts; `mode = "build"` / `"prebuild"` selects explicitly when both forms are present.
|
||||
- **Prebuilt dependency examples** — documentation now shows the `dcr-math-lib` Git repository and recommended build/prebuilt layouts for both `git` and `path`.
|
||||
- **Transitive dependency builds and cycle reporting** — DCR recursively builds DCR packages and reports a clear dependency chain for cycles.
|
||||
- **Bare path arguments for `dcr add`** — `dcr add mylib ../mylib` now works in addition to the explicit `path:` form.
|
||||
- **Removed `install.sh` / `install.ps1` / `install_bsd.sh`** — superseded by `dcrup`.
|
||||
|
||||
## 0.8.5 (2026-08-08)
|
||||
|
||||
- **`package.dcr-version`** — optional CLI version pin in `dcr.toml`.
|
||||
`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
|
||||
|
||||
@@ -15,6 +15,8 @@ dcr add mylib path:./lib # explicit path prefix
|
||||
dcr add mylib git:https://github.com/user/repo # explicit git prefix
|
||||
dcr add mylib github:user/repo # GitHub shorthand
|
||||
dcr add mylib gitlab:user/repo # GitLab shorthand
|
||||
dcr add fmt pkg-config:fmt # system package via pkg-config
|
||||
dcr add openssl pkg:openssl,libcrypto # multiple pkg-config packages
|
||||
```
|
||||
|
||||
Source prefixes:
|
||||
@@ -22,6 +24,7 @@ Source prefixes:
|
||||
- `git:` — generic git URL
|
||||
- `github:` — expands to `https://github.com/<user>/<repo>`
|
||||
- `gitlab:` — expands to `https://gitlab.com/<user>/<repo>`
|
||||
- `pkg-config:` / `pkg:` — one or more packages resolved by `pkg-config`
|
||||
- `http://` / `https://` / `git@` — full URL
|
||||
|
||||
Flags:
|
||||
@@ -29,7 +32,12 @@ Flags:
|
||||
- `--tag <name>` — git tag
|
||||
- `--rev <hash>` — git commit
|
||||
|
||||
If source is omitted — DCR searches connected registries.
|
||||
If source is omitted — DCR searches connected registries. A bare relative or absolute path is accepted as a path dependency.
|
||||
|
||||
For `pkg-config` dependencies, the referenced packages must already be installed
|
||||
and provide `.pc` files. DCR invokes `pkg-config` during the build (using
|
||||
`PATH` and `PKG_CONFIG_PATH`) and forwards the returned compiler and linker
|
||||
flags; it does not download or build these packages.
|
||||
|
||||
## `dcr tree`
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ sidebar_label: Contributing
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dexoron/dcr
|
||||
# or a source mirror: GitVerse / SourceCraft / SourceForge / GitLab (see README)
|
||||
cd dcr
|
||||
cargo build
|
||||
```
|
||||
|
||||
@@ -61,6 +61,15 @@ Use `kind = "flat-bin"` (NASM `-f bin`) and optional `[archive]` for a FAT disk
|
||||
|
||||
See [OS-dev recipe](/docs/recipes/os-flat-bin-archive).
|
||||
|
||||
## How do I choose a download mirror?
|
||||
|
||||
`dcrup install` uses `--mirror auto` by default and tries **cf → gh → sf → ya**.
|
||||
|
||||
```bash
|
||||
dcrup install stable --mirror cf
|
||||
export DCRUP_MIRROR=ya
|
||||
```
|
||||
|
||||
## Lock file
|
||||
|
||||
`dcr.lock` is created during builds (`dcr build`) when registry dependencies are present. To force an update — delete `dcr.lock` and run `dcr build`.
|
||||
|
||||
@@ -11,7 +11,7 @@ Install and switch DCR versions (stable / dev / night, pin `0.8.2`, optional bui
|
||||
### Linux / macOS / BSD / Windows (bash)
|
||||
|
||||
```sh
|
||||
curl -fsSL https://sh.dcr-tool.ru | sh -s -- self-install
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.dcr-tool.ru | sh -s -- self-install
|
||||
export PATH="$HOME/.dcr/bin:$PATH"
|
||||
dcrup install stable
|
||||
```
|
||||
@@ -19,36 +19,29 @@ dcrup install stable
|
||||
Non-interactive install of DCR in one shot:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://sh.dcr-tool.ru | sh -s -- install stable
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.dcr-tool.ru | sh -s -- install stable
|
||||
export PATH="$HOME/.dcr/bin:$PATH"
|
||||
```
|
||||
|
||||
### Windows (PowerShell)
|
||||
|
||||
```powershell
|
||||
irm https://ps1.dcr-tool.ru | iex
|
||||
# or download then run:
|
||||
# irm https://ps1.dcr-tool.ru -OutFile dcrup.ps1
|
||||
# powershell -File .\dcrup.ps1 self-install
|
||||
& ([scriptblock]::Create((irm https://ps1.dcr-tool.ru))) self-install
|
||||
# $env:Path += ";$env:USERPROFILE\.dcr\bin"
|
||||
dcrup install stable
|
||||
```
|
||||
|
||||
Optional cmd bootstrap (if you prefer `curl` of the cmd shim):
|
||||
|
||||
```bat
|
||||
curl -fsSL -o dcrup.cmd https://cmd.dcr-tool.ru
|
||||
```
|
||||
|
||||
After `self-install`, the command is **`dcrup`** (no `.sh` / `.ps1`): shims live in `~/.dcr/bin` (Unix) or `%USERPROFILE%\.dcr\bin` (Windows `dcrup.cmd`).
|
||||
After `self-install`, the command is **`dcrup`** (no `.sh` / `.ps1`) and is installed in `~/.dcr/bin` (Unix) or `%USERPROFILE%\.dcr\bin` (Windows).
|
||||
|
||||
### Common dcrup commands
|
||||
|
||||
```sh
|
||||
dcrup install stable # latest stable prebuilt
|
||||
dcrup install stable # latest stable prebuilt (auto: cf → gh → sf → ya)
|
||||
dcrup install 0.8.2 # pin → 0.8.2@stable
|
||||
dcrup install 0.8.2@dev
|
||||
dcrup install stable --libc musl # Linux: musl asset (default: gnu)
|
||||
dcrup install stable --mirror cf # CloudFlare CDN
|
||||
dcrup install stable --mirror ya # Yandex CDN (RU fallback)
|
||||
dcrup install stable --build # cargo build --features archive
|
||||
dcrup install night # always build from branch dev HEAD
|
||||
dcrup default 0.8.2
|
||||
@@ -56,10 +49,28 @@ dcrup update
|
||||
dcrup list
|
||||
dcrup show
|
||||
dcrup which
|
||||
dcrup uninstall 0.8.2 # remove one installed version
|
||||
dcrup uninstall --all # remove all installed versions
|
||||
```
|
||||
|
||||
Layout: `~/.dcr/toolchains/<id>/dcr` and `~/.dcr/bin/dcr` → active version.
|
||||
|
||||
### Download mirrors
|
||||
|
||||
Prebuilt installs try **cf → gh → sf → ya** unless you pin a host.
|
||||
|
||||
| Code | URL | Notes |
|
||||
|------|-----|--------|
|
||||
| `cf` | `https://cdn-global.dcr-tool.ru/<tag>/<asset>` | CloudFlare CDN (worldwide) |
|
||||
| `gh` | `https://github.com/dexoron/dcr/releases/download/<tag>/<asset>` | GitHub Releases |
|
||||
| `sf` | `https://sourceforge.net/projects/dcr-tool/files/<tag>/<asset>/download` | SourceForge |
|
||||
| `ya` | `https://cdn-ru.dcr-tool.ru/<tag>/<asset>` | Yandex CDN (RU fallback) |
|
||||
|
||||
```sh
|
||||
dcrup install stable --mirror auto # default
|
||||
export DCRUP_MIRROR=cf # persist for this shell
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Arch Linux (AUR)
|
||||
|
||||
@@ -6,6 +6,13 @@ sidebar_label: IDE Integration
|
||||
|
||||
## VS Code
|
||||
|
||||
Install the recommended [DCR extension by Dexoron-Labs](https://marketplace.visualstudio.com/items?itemName=Dexoron-Labs.dcr). Do not install Microsoft C/C++ extensions for DCR projects: DCR uses `clangd`. MS C/C++ is acceptable only when you need its debugger.
|
||||
|
||||
Recommended companion extensions:
|
||||
|
||||
- [Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml)
|
||||
- [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb), or MS C/C++ for debugger support only
|
||||
|
||||
```bash
|
||||
dcr gen vscode
|
||||
```
|
||||
|
||||
@@ -43,7 +43,8 @@ Main project configuration file. Located at the project root.
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `name` | yes | Project name |
|
||||
| `version` | yes | Semantic version |
|
||||
| `version` | yes | Semantic version of the **project** |
|
||||
| `dcr-version` | no | Semver of the **dcr tool** this project targets. Written by `dcr new` / `dcr init` to the current tool version. |
|
||||
| `type` | no | `app`, `lib`, `none` (defaults to `"none"`) |
|
||||
| `license` | no | SPDX license identifier |
|
||||
| `author` | no | Author |
|
||||
@@ -52,11 +53,26 @@ Main project configuration file. Located at the project root.
|
||||
[package]
|
||||
name = "my-app"
|
||||
version = "0.1.0"
|
||||
dcr-version = "0.8.4"
|
||||
type = "app"
|
||||
license = "MIT"
|
||||
author = "John Doe"
|
||||
```
|
||||
|
||||
### `package.dcr-version`
|
||||
|
||||
Optional pin of the DCR CLI the project was written for. On every `dcr.toml` load:
|
||||
|
||||
| Situation | Behaviour |
|
||||
|-----------|-----------|
|
||||
| Field missing / empty | Warning: add `dcr-version` (set by `dcr new` / `dcr init`) |
|
||||
| Running dcr **older** than pin | Warning: upgrade dcr (`dcr --update`) |
|
||||
| Running dcr **newer** than pin | Warning: review changelog, bump the pin when ready |
|
||||
| Equal | Silent |
|
||||
| Invalid semver | Warning once, field ignored |
|
||||
|
||||
Never fails the command — warnings only. Pre-release suffixes (`1.0.0-dev`) are ignored for comparison (`1.0.0-dev` ≡ `1.0.0`).
|
||||
|
||||
## [build]
|
||||
|
||||
...
|
||||
@@ -95,7 +111,11 @@ author = "John Doe"
|
||||
| `qt` | `false` | Enable automatic Qt meta-object handling (MOC, UIC, RCC) |
|
||||
|
||||
Settings from raw config (not in typed struct):
|
||||
- `pkg_config` — list of pkg-config packages
|
||||
- `pkg_config` — list of packages queried for build-wide compiler/linker flags
|
||||
|
||||
Dependencies can also declare a system package directly, for example
|
||||
`fmt = { pkg-config = "fmt" }`. This records the package in `dcr.lock` and applies
|
||||
the flags returned by `pkg-config --cflags` and `pkg-config --libs`.
|
||||
- `ldscript` — linker script path
|
||||
- `build.steps` / `build.post_steps` — codegen steps
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ sidebar_label: Dependencies
|
||||
|
||||
## Formats
|
||||
|
||||
The `[dependencies]` section supports three formats:
|
||||
The `[dependencies]` section supports registry, path, Git, and `pkg-config` dependencies:
|
||||
|
||||
### String (registry)
|
||||
|
||||
@@ -23,10 +23,10 @@ The version string is used as-is for registry lookup.
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
fmt = { git = "https://github.com/fmtlib/fmt", tag = "10.1.1" }
|
||||
dcr-math = { git = "https://github.com/dexoron/dcr-math-lib.git", tag = "v0.1.0" }
|
||||
```
|
||||
|
||||
Fields: `git`, `branch`, `tag`, `rev`.
|
||||
Fields: `git`, `branch`, `tag`, `rev`, `mode`, `include`, `lib`, `libs`.
|
||||
|
||||
### Table (path)
|
||||
|
||||
@@ -35,6 +35,32 @@ Fields: `git`, `branch`, `tag`, `rev`.
|
||||
mylib = { path = "../mylib" }
|
||||
```
|
||||
|
||||
For a prebuilt external library, specify its layout explicitly. The three
|
||||
arrays are paths relative to the dependency root (`include`, `lib`) and linker
|
||||
names (`libs`):
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
dcr-math = { path = "../deps/dcr-math", include = ["include"], lib = ["lib"], libs = ["dcr-math"] }
|
||||
```
|
||||
|
||||
### Table (`pkg-config`)
|
||||
|
||||
Use an installed `.pc` package when the library is provided by the operating system
|
||||
or another system package manager. DCR invokes `pkg-config --cflags` and
|
||||
`pkg-config --libs`, preserving include, library, linker, and other flags:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
fmt = { pkg-config = "fmt" }
|
||||
openssl = { pkg-config = ["openssl", "libcrypto"] }
|
||||
```
|
||||
|
||||
The `pkg-config` executable must be available in `PATH`. The named `.pc` files must
|
||||
be available through pkg-config's standard search paths or via `PKG_CONFIG_PATH`.
|
||||
These dependencies are not downloaded or built by DCR; they are recorded in
|
||||
`dcr.lock` with a `pkg-config+...` source.
|
||||
|
||||
## Registry
|
||||
|
||||
DCR uses a package registry for dependency lookup by name.
|
||||
@@ -42,7 +68,7 @@ DCR uses a package registry for dependency lookup by name.
|
||||
```toml
|
||||
# ~/.dcr/config.toml
|
||||
[registry.default]
|
||||
url = "https://index.dcr.pm"
|
||||
url = "https://index.dcr.dexoron.tech"
|
||||
priority = 1
|
||||
```
|
||||
|
||||
@@ -50,7 +76,23 @@ Registry priority: order in `config.toml`. The `DCR_INDEX_PATH` variable overrid
|
||||
|
||||
## Git dependencies
|
||||
|
||||
Git dependencies are parsed and recorded in `dcr.lock`. DCR supports specifying branch/tag/rev for git sources:
|
||||
Git dependencies are cloned into `~/.dcr/git/`; their exact checked-out commit is written to `dcr.lock`.
|
||||
|
||||
Both Git and path dependencies have two modes:
|
||||
|
||||
- **DCR package:** the source has `dcr.toml`. DCR builds it before the consumer and uses the package name from its manifest for linking.
|
||||
- **Prebuilt library:** it provides existing headers and binaries through `include`, `lib`, and `libs`. DCR passes them to the compiler/linker without building the dependency.
|
||||
|
||||
The mode is normally detected automatically. Use `mode = "build"` or `mode = "prebuild"` when a source contains both a manifest and prebuilt artifacts and you need to choose explicitly.
|
||||
|
||||
`dcr-math-lib` publishes both forms. Select its prebuilt library explicitly:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
dcr-math = { git = "https://github.com/dexoron/dcr-math-lib.git", tag = "v0.1.0", mode = "prebuild", include = ["include"], lib = ["lib"], libs = ["dcr-math"] }
|
||||
```
|
||||
|
||||
DCR supports specifying branch/tag/rev for git sources:
|
||||
|
||||
- `branch` — switch to a branch
|
||||
- `tag` — switch to a tag
|
||||
@@ -59,7 +101,7 @@ Git dependencies are parsed and recorded in `dcr.lock`. DCR supports specifying
|
||||
|
||||
## Path dependencies
|
||||
|
||||
Local paths. DCR automatically discovers include and lib directories from the neighbor's `dcr.toml`.
|
||||
Local paths may point at a DCR package or a prebuilt external library. A path DCR package is built before its consumer, including its own path/git dependencies. DCR detects dependency cycles. A prebuilt library is never built by DCR; it is linked from the directories declared in its dependency entry.
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
@@ -67,18 +109,51 @@ mylib = { path = "/abs/path/to/lib" }
|
||||
mylib = { path = "../relative/path" }
|
||||
```
|
||||
|
||||
*Note: Header resolution automatically checks the packaged `target/include` directory of path dependencies, ensuring consumer packages can find headers from compiled static or dynamic libraries.*
|
||||
For DCR packages, the package name from `dcr.toml` is used for linking (so the dependency key may differ from the library name). Header resolution includes the packaged `target/include` directory and link search includes `target/lib`.
|
||||
|
||||
### Recommended library layouts
|
||||
|
||||
Buildable DCR library:
|
||||
|
||||
```text
|
||||
dcr-math-lib/
|
||||
├── dcr.toml
|
||||
└── src/
|
||||
├── dcr_math.c
|
||||
└── dcr_math.h
|
||||
```
|
||||
|
||||
Prebuilt library:
|
||||
|
||||
```text
|
||||
dcr-math-lib/
|
||||
├── include/
|
||||
│ └── dcr_math.h
|
||||
└── lib/
|
||||
└── libdcr-math.a # or .so / .lib
|
||||
```
|
||||
|
||||
`libs` contains linker names without the `lib` prefix and file extension: use
|
||||
`["dcr-math"]` for `libdcr-math.a`.
|
||||
|
||||
If one repository contains both forms, keep the prebuilt files in `include/` and `lib/` and select the mode explicitly:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
dcr-math = { git = "https://github.com/dexoron/dcr-math-lib.git", tag = "v0.1.0", mode = "prebuild", include = ["include"], lib = ["lib"], libs = ["dcr-math"] }
|
||||
```
|
||||
|
||||
For a local copy, replace `git` with `path = "../dcr-math-lib"`. In `prebuild` mode DCR does not run the dependency build, even when `dcr.toml` is present.
|
||||
|
||||
## dcr.lock
|
||||
|
||||
Dependency lock file. Contains package names and sources.
|
||||
|
||||
Created during `dcr build` when registry dependencies are present. Not updated during `dcr add` — only on the next `dcr build`.
|
||||
Created during `dcr build` for resolved registry, path, and Git dependencies. Not updated during `dcr add` — only on the next `dcr build`.
|
||||
|
||||
## Resolution process
|
||||
|
||||
1. Load all dependencies (registry → git → path)
|
||||
2. For path deps: recursively read their `dcr.toml`
|
||||
3. For registry deps: search `index.json`
|
||||
4. For git deps: clone to cache
|
||||
5. Collect `include_dirs`, `lib_dirs`, `libs` for the compiler
|
||||
1. Resolve dependency sources (registry → git → path)
|
||||
2. Fetch git dependencies into `~/.dcr/git/` and check out the requested ref
|
||||
3. Build DCR package dependencies recursively
|
||||
4. Collect `include_dirs`, `lib_dirs`, `libs` for the compiler
|
||||
|
||||
@@ -57,3 +57,27 @@ export DCR_INDEX_PATH=/custom/path/index.json
|
||||
```
|
||||
|
||||
Default: `~/.dcr/index.json`.
|
||||
|
||||
## DCRUP_MIRROR
|
||||
|
||||
Download mirror for `dcrup` (prebuilt binaries only).
|
||||
|
||||
```bash
|
||||
export DCRUP_MIRROR=auto # cf → gh → sf → ya (default)
|
||||
export DCRUP_MIRROR=cf # CloudFlare CDN
|
||||
export DCRUP_MIRROR=gh # GitHub Releases
|
||||
export DCRUP_MIRROR=sf # SourceForge
|
||||
export DCRUP_MIRROR=ya # Yandex CDN (RU fallback)
|
||||
```
|
||||
|
||||
CLI override: `dcrup install stable --mirror cf`.
|
||||
|
||||
## DCRUP_HOME / DCRUP_BIN / DCRUP_REPO
|
||||
|
||||
Installer layout and source clone URL:
|
||||
|
||||
```bash
|
||||
export DCRUP_HOME=~/.dcr
|
||||
export DCRUP_BIN=~/.dcr/bin
|
||||
export DCRUP_REPO=https://github.com/dexoron/dcr
|
||||
```
|
||||
|
||||
160
install.ps1
160
install.ps1
@@ -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'
|
||||
253
install.sh
253
install.sh
@@ -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 "$@"
|
||||
241
install_bsd.sh
241
install_bsd.sh
@@ -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 "$@"
|
||||
@@ -25,6 +25,9 @@ Generic git repository.
|
||||
.B path:./path/to/lib
|
||||
Local path dependency.
|
||||
.TP
|
||||
.B pkg-config:name
|
||||
System pkg-config dependency (also accepts \fBpkg:\fR or comma-separated packages).
|
||||
.TP
|
||||
.B <version>
|
||||
Version from registry.
|
||||
.SH OPTIONS
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.TH DCR-BUILD 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
|
||||
.TH DCR-BUILD 1 "2026-08-04" "dcr 0.8.4" "DCR Manual"
|
||||
.SH NAME
|
||||
dcr-build \- Compile a DCR project
|
||||
.SH SYNOPSIS
|
||||
@@ -9,8 +9,15 @@ dcr-build \- Compile a DCR project
|
||||
[\fB\-\-clean\fR]
|
||||
[\fB\-\-verbose\fR]
|
||||
[\fB\-\-workspace\fR \fI<name>\fR]
|
||||
[\fB\-\-print\-artifact\-path\fR]
|
||||
.SH DESCRIPTION
|
||||
Compiles the project. Default profile is \fB\-\-debug\fR.
|
||||
Compiles the project. Default profile is
|
||||
.B \-\-debug .
|
||||
On success, updates
|
||||
.B .dcr/build-info.json
|
||||
and
|
||||
.B .dcr/toolchain.json
|
||||
when applicable.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-\-debug
|
||||
@@ -33,5 +40,9 @@ Print detailed build output including compiler commands.
|
||||
.TP
|
||||
.B \-\-workspace \fI<name>\fR
|
||||
Build a specific workspace member.
|
||||
.TP
|
||||
.B \-\-print\-artifact\-path
|
||||
After a successful build, print the absolute path of the main artifact as a single line on stdout.
|
||||
.SH SEE ALSO
|
||||
.BR dcr (1)
|
||||
.BR dcr (1),
|
||||
.BR dcr-gen (1)
|
||||
|
||||
@@ -1,23 +1,67 @@
|
||||
.TH DCR-GEN 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
|
||||
.TH DCR-GEN 1 "2026-08-04" "dcr 0.8.4" "DCR Manual"
|
||||
.SH NAME
|
||||
dcr-gen \- Generate IDE and tooling integration files
|
||||
.SH SYNOPSIS
|
||||
.B dcr gen \fI<subcommand>\fR
|
||||
[\fB\-\-debug\fR | \fB\-\-release\fR]
|
||||
[\fB\-\-quiet\fR | \fB\-q\fR]
|
||||
.SH DESCRIPTION
|
||||
Generates IDE and tooling integration files for DCR projects.
|
||||
Machine-readable build facts are written under
|
||||
.B .dcr/
|
||||
(compile database, build-info, toolchain).
|
||||
.SH SUBCOMMANDS
|
||||
.TP
|
||||
.B project-info
|
||||
Print project metadata as JSON to stdout.
|
||||
Print project metadata as JSON to stdout (absolute paths, artifact_path, target_dir).
|
||||
Also updates
|
||||
.B .dcr/build-info.json
|
||||
and
|
||||
.B .dcr/toolchain.json .
|
||||
.TP
|
||||
.B compile-commands
|
||||
Generate \fBcompile_commands.json\fR for IDE tooling.
|
||||
Generate
|
||||
.B .dcr/compile_commands.json
|
||||
(atomic write) and ensure
|
||||
.B .clangd
|
||||
points at
|
||||
.B .dcr
|
||||
as CompilationDatabase.
|
||||
.TP
|
||||
.B vscode
|
||||
Generate \fB.vscode/\fR integration files (tasks, launch configurations).
|
||||
Generate
|
||||
.B .vscode/
|
||||
integration files (tasks, launch configurations) and compile commands under
|
||||
.B .dcr/ .
|
||||
.TP
|
||||
.B clion
|
||||
Generate \fB.idea/\fR integration files for CLion.
|
||||
Generate
|
||||
.B .idea/
|
||||
integration files for CLion and compile commands under
|
||||
.B .dcr/ .
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-\-debug
|
||||
Use debug profile (default).
|
||||
.TP
|
||||
.B \-\-release
|
||||
Use release profile.
|
||||
.TP
|
||||
.B \-\-quiet\fR, \fB\-q
|
||||
Suppress human-readable success messages on stdout.
|
||||
.SH FILES
|
||||
.TP
|
||||
.B .dcr/compile_commands.json
|
||||
JSON Compilation Database (moved from project root; breaking change in 0.8.4).
|
||||
.TP
|
||||
.B .dcr/build-info.json
|
||||
Last known artifact path, profile, compiler.
|
||||
.TP
|
||||
.B .dcr/toolchain.json
|
||||
Resolved compiler/debugger (and Qt tools if configured).
|
||||
.TP
|
||||
.B .clangd
|
||||
Created once with CompilationDatabase: .dcr; not overwritten if customized.
|
||||
.SH SEE ALSO
|
||||
.BR dcr (1),
|
||||
.BR dcr-build (1)
|
||||
|
||||
@@ -8,9 +8,13 @@ dcr-run \- Build and run a DCR project
|
||||
[\fB\-\-force\fR]
|
||||
[\fB\-\-clean\fR]
|
||||
[\fB\-\-verbose\fR]
|
||||
[\fB\-\-\fR \fI<args>...\fR]
|
||||
.SH DESCRIPTION
|
||||
Builds and runs the project. Only available for projects with
|
||||
\fBkind = "bin"\fR.
|
||||
Arguments after a bare
|
||||
.B \-\-
|
||||
are forwarded to the built binary (cargo-style).
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-\-debug
|
||||
@@ -30,6 +34,14 @@ Clean before building.
|
||||
.TP
|
||||
.B \-\-verbose
|
||||
Print detailed build output.
|
||||
.TP
|
||||
.B \-\- \fI<args>...\fR
|
||||
Arguments passed to the built binary.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
dcr run --release -- --test_help
|
||||
dcr run -- --foo bar
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR dcr (1),
|
||||
.BR dcr-build (1)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.TH DCR 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
|
||||
.TH DCR 1 "2026-08-31" "dcr 0.9.1" "DCR Manual"
|
||||
.SH NAME
|
||||
dcr \- Cargo\-like build tool and package manager for C/C++ projects
|
||||
.SH SYNOPSIS
|
||||
@@ -120,6 +120,9 @@ Generic git repository.
|
||||
.B path:./path/to/lib
|
||||
Local path dependency.
|
||||
.TP
|
||||
.B pkg-config:name
|
||||
System pkg-config dependency (also accepts \fBpkg:name1,name2\fR).
|
||||
.TP
|
||||
.B \fI<version>\fR
|
||||
Version from registry (e.g. \fB1.2.3\fR).
|
||||
.SH GEN SUBCOMMANDS
|
||||
@@ -128,7 +131,7 @@ Version from registry (e.g. \fB1.2.3\fR).
|
||||
Print project metadata as JSON to stdout.
|
||||
.TP
|
||||
.B compile-commands
|
||||
Generate \fBcompile_commands.json\fR for IDE tooling.
|
||||
Generate \fB.dcr/compile_commands.json\fR for IDE tooling.
|
||||
.TP
|
||||
.B vscode
|
||||
Generate \fB.vscode/\fR integration files (tasks, launch configurations).
|
||||
@@ -154,6 +157,11 @@ Project name (required).
|
||||
.B package.version
|
||||
Project version (default: \fB0.1.0\fR).
|
||||
.TP
|
||||
.B package.dcr\-version
|
||||
Semver of the DCR tool this project targets. Set automatically by
|
||||
\fBdcr new\fR / \fBdcr init\fR. Missing pin, or an installed \fBdcr\fR older/newer
|
||||
than the pin, prints a warning (commands still run).
|
||||
.TP
|
||||
.B build.language
|
||||
Programming language: \fBc\fR or \fBc++\fR (default: \fBc\fR).
|
||||
.TP
|
||||
|
||||
@@ -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!")
|
||||
149
src/cli/add.rs
149
src/cli/add.rs
@@ -15,43 +15,68 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::build_config::Config;
|
||||
use crate::core::deps::register;
|
||||
use crate::utils::fs::find_project_root;
|
||||
use crate::utils::log::error;
|
||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
|
||||
use toml::Value;
|
||||
use toml::map::Map;
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::core::{build_config::Config, deps::register};
|
||||
use crate::utils::cli_styles::{Colorize, HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST, SUCCESS_ST};
|
||||
use crate::utils::fs::find_project_root;
|
||||
use crate::utils::log::sprintln;
|
||||
use toml::{Value, map::Map};
|
||||
|
||||
/// Parsed arguments for the `dcr add` command.
|
||||
///
|
||||
/// Exactly one of `path`, `git`, `pkg_config`, or `version_from_registry` is expected to be set,
|
||||
/// depending on how the dependency source was specified.
|
||||
pub struct AddArgs {
|
||||
/// Dependency name as it will appear under `[dependencies]` in `dcr.toml`.
|
||||
pub name: String,
|
||||
/// Local filesystem path when the source uses the `path:` prefix.
|
||||
pub path: Option<String>,
|
||||
/// Git repository URL when the source is a git/GitHub/GitLab URL or prefix.
|
||||
pub git: Option<String>,
|
||||
/// Optional git branch to pin the dependency to.
|
||||
pub branch: Option<String>,
|
||||
/// Optional git tag to pin the dependency to.
|
||||
pub tag: Option<String>,
|
||||
/// Optional git commit hash (revision) to pin the dependency to.
|
||||
pub rev: Option<String>,
|
||||
/// Version string resolved from the package registry when no explicit source is given.
|
||||
pub version_from_registry: Option<String>,
|
||||
/// Package names resolved through pkg-config.
|
||||
pub pkg_config: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Adds a dependency to the current project's `dcr.toml`.
|
||||
///
|
||||
/// Parses CLI arguments, locates the project root, builds a TOML dependency value
|
||||
/// (path, git with optional ref, or registry version), and writes it under
|
||||
/// `dependencies.<name>`.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `args`: Tokens after `dcr add` (name, source, optional `--branch`/`--tag`/`--rev`).
|
||||
///
|
||||
/// # Returns
|
||||
/// Process exit code: `0` on success, non-zero on usage or I/O errors.
|
||||
pub fn add(args: &[String]) -> i32 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
printc("USAGE:", BOLD_GREEN);
|
||||
printc(
|
||||
" dcr add <name> <source> [--branch <b>] [--tag <t>] [--rev <r>]",
|
||||
BOLD_CYAN,
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
|
||||
sprintln!(
|
||||
HELP_EXAMPLES_ST,
|
||||
" dcr add <name> <source> [--branch <b>] [--tag <t>] [--rev <r>]"
|
||||
);
|
||||
println!();
|
||||
printc("DESCRIPTION:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
|
||||
println!(" Adds a dependency to the project.");
|
||||
println!();
|
||||
printc("SOURCES:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "SOURCES:");
|
||||
println!(" github:user/repo GitHub repository");
|
||||
println!(" gitlab:user/repo GitLab repository");
|
||||
println!(" git:host.com/user/repo Generic git repository");
|
||||
println!(" path:./path/to/lib Local path dependency");
|
||||
println!(" pkg-config:fmt System pkg-config dependency");
|
||||
println!(" <version> Version from registry");
|
||||
println!();
|
||||
printc("OPTIONS:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "OPTIONS:");
|
||||
println!(" --branch <b> Use a specific branch");
|
||||
println!(" --tag <t> Use a specific tag");
|
||||
println!(" --rev <r> Use a specific commit hash");
|
||||
@@ -66,7 +91,7 @@ pub fn add(args: &[String]) -> i32 {
|
||||
let start_dir = match std::env::current_dir() {
|
||||
Ok(dir) => dir,
|
||||
Err(_) => {
|
||||
error("Failed to determine current directory");
|
||||
error!("Failed to determine current directory");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
@@ -74,11 +99,11 @@ pub fn add(args: &[String]) -> i32 {
|
||||
let root = match find_project_root(&start_dir) {
|
||||
Ok(Some(dir)) => dir,
|
||||
Ok(None) => {
|
||||
error("dcr.toml file not found");
|
||||
error!("dcr.toml file not found");
|
||||
return 1;
|
||||
}
|
||||
Err(_) => {
|
||||
error("Failed to find project root");
|
||||
error!("Failed to find project root");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
@@ -86,11 +111,12 @@ pub fn add(args: &[String]) -> i32 {
|
||||
let mut config = match Config::open(&root.join("dcr.toml").to_string_lossy()) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(err) => {
|
||||
error(&err.to_string());
|
||||
error!("{err}");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
// Build the TOML value written under dependencies.<name>
|
||||
let dep_value = if let Some(git_url) = add_args.git {
|
||||
if add_args.branch.is_none() && add_args.tag.is_none() && add_args.rev.is_none() {
|
||||
let mut table = Map::new();
|
||||
@@ -110,6 +136,17 @@ pub fn add(args: &[String]) -> i32 {
|
||||
}
|
||||
Value::Table(table)
|
||||
}
|
||||
} else if let Some(packages) = add_args.pkg_config {
|
||||
let mut table = Map::new();
|
||||
if packages.len() == 1 {
|
||||
table.insert("pkg-config".to_string(), Value::String(packages[0].clone()));
|
||||
} else {
|
||||
table.insert(
|
||||
"pkg-config".to_string(),
|
||||
Value::Array(packages.into_iter().map(Value::String).collect()),
|
||||
);
|
||||
}
|
||||
Value::Table(table)
|
||||
} else if let Some(path) = add_args.path {
|
||||
let mut table = Map::new();
|
||||
table.insert("path".to_string(), Value::String(path));
|
||||
@@ -117,32 +154,38 @@ pub fn add(args: &[String]) -> i32 {
|
||||
} else if let Some(version) = add_args.version_from_registry {
|
||||
Value::String(version)
|
||||
} else {
|
||||
error("Dependency source (path or git) must be provided");
|
||||
error!("Dependency source (path, git, or pkg-config) must be provided");
|
||||
return 1;
|
||||
};
|
||||
|
||||
let key = format!("dependencies.{}", add_args.name);
|
||||
if let Err(err) = config.edit(&key, dep_value) {
|
||||
error(&format!("Failed to update dcr.toml: {}", err));
|
||||
error!("Failed to update dcr.toml: {err}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
println!(
|
||||
" {} dependency `{}` to dcr.toml",
|
||||
colored("Added", BOLD_GREEN),
|
||||
"Added".style(SUCCESS_ST),
|
||||
add_args.name
|
||||
);
|
||||
0
|
||||
}
|
||||
|
||||
/// Parses CLI arguments into an [`AddArgs`] value.
|
||||
///
|
||||
/// Expects `<name> [source] [--branch|--tag|--rev ...]`. If `source` is omitted,
|
||||
/// the package registry is queried for a default version. Source strings may use
|
||||
/// `path:`, `github:`, `gitlab:`, or `git:` prefixes, or a full git URL.
|
||||
fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|
||||
if args.is_empty() {
|
||||
error("Usage: dcr add <name> <source> [--branch <branch> | --tag <tag> | --rev <rev>]");
|
||||
error("Sources:");
|
||||
error(" github:user/repo -> github.com/user/repo");
|
||||
error(" gitlab:user/repo -> gitlab.com/user/repo");
|
||||
error(" git:host.com/user/repo -> host.com/user/repo");
|
||||
error(" path:./path/to/lib -> local path");
|
||||
error!("Usage: dcr add <name> <source> [--branch <branch> | --tag <tag> | --rev <rev>]");
|
||||
error!("Sources:");
|
||||
error!(" github:user/repo -> github.com/user/repo");
|
||||
error!(" gitlab:user/repo -> gitlab.com/user/repo");
|
||||
error!(" git:host.com/user/repo -> host.com/user/repo");
|
||||
error!(" path:./path/to/lib -> local path");
|
||||
error!(" pkg-config:fmt -> system pkg-config package");
|
||||
return Err(1);
|
||||
}
|
||||
|
||||
@@ -155,7 +198,7 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|
||||
let mut iter = args.iter();
|
||||
if let Some(n) = iter.next() {
|
||||
if n.starts_with("--") {
|
||||
error("First argument must be the dependency name");
|
||||
error!("First argument must be the dependency name");
|
||||
return Err(1);
|
||||
}
|
||||
name = n.clone();
|
||||
@@ -163,12 +206,12 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|
||||
|
||||
if let Some(s) = iter.next() {
|
||||
if s.starts_with("--") {
|
||||
error("Second argument must be the dependency source");
|
||||
error!("Second argument must be the dependency source");
|
||||
return Err(1);
|
||||
}
|
||||
source_spec = s.clone();
|
||||
} else {
|
||||
// No source provided — try registry auto-lookup
|
||||
// No source provided — resolve version from the package registry
|
||||
match register::resolve_package_from_registry(&name) {
|
||||
Ok(info) => {
|
||||
let version = info
|
||||
@@ -182,7 +225,7 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|
||||
)
|
||||
})
|
||||
.map_err(|(msg, code)| {
|
||||
error(&msg);
|
||||
error!("{msg}");
|
||||
code
|
||||
})?;
|
||||
return Ok(AddArgs {
|
||||
@@ -193,10 +236,11 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|
||||
tag: None,
|
||||
rev: None,
|
||||
version_from_registry: Some(version.to_string()),
|
||||
pkg_config: None,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
error(&format!("Cannot add `{}` without source: {}", name, e));
|
||||
error!("Cannot add `{}` without source: {}", name, e);
|
||||
return Err(1);
|
||||
}
|
||||
}
|
||||
@@ -207,26 +251,26 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|
||||
"--branch" => {
|
||||
branch = iter.next().cloned();
|
||||
if branch.is_none() {
|
||||
error("--branch requires a value");
|
||||
error!("--branch requires a value");
|
||||
return Err(1);
|
||||
}
|
||||
}
|
||||
"--tag" => {
|
||||
tag = iter.next().cloned();
|
||||
if tag.is_none() {
|
||||
error("--tag requires a value");
|
||||
error!("--tag requires a value");
|
||||
return Err(1);
|
||||
}
|
||||
}
|
||||
"--rev" => {
|
||||
rev = iter.next().cloned();
|
||||
if rev.is_none() {
|
||||
error("--rev requires a value");
|
||||
error!("--rev requires a value");
|
||||
return Err(1);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
error(&format!("Unknown argument: {}", arg));
|
||||
error!("Unknown argument: {arg}");
|
||||
return Err(1);
|
||||
}
|
||||
}
|
||||
@@ -234,8 +278,25 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|
||||
|
||||
let mut path = None;
|
||||
let mut git = None;
|
||||
let mut pkg_config = None;
|
||||
|
||||
if let Some(p) = source_spec.strip_prefix("path:") {
|
||||
// Map source prefixes / bare URLs to path or git fields
|
||||
if let Some(p) = source_spec
|
||||
.strip_prefix("pkg-config:")
|
||||
.or_else(|| source_spec.strip_prefix("pkg:"))
|
||||
{
|
||||
let packages = p
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|name| !name.is_empty())
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
if packages.is_empty() {
|
||||
error!("pkg-config source must name at least one package");
|
||||
return Err(1);
|
||||
}
|
||||
pkg_config = Some(packages);
|
||||
} else if let Some(p) = source_spec.strip_prefix("path:") {
|
||||
path = Some(p.to_string());
|
||||
} else if let Some(g) = source_spec.strip_prefix("github:") {
|
||||
git = Some(format!("https://github.com/{}", g));
|
||||
@@ -243,10 +304,10 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|
||||
git = Some(format!("https://gitlab.com/{}", g));
|
||||
} else if let Some(g) = source_spec.strip_prefix("git:") {
|
||||
if g.contains('/') && !g.contains('.') {
|
||||
// git:user/repo -> default to github
|
||||
// git:user/repo with no host — default to GitHub
|
||||
git = Some(format!("https://github.com/{}", g));
|
||||
} else {
|
||||
// git:host.com/user/repo
|
||||
// git:host.com/user/repo or an already-qualified URL
|
||||
let url = if g.starts_with("http") || g.starts_with("git@") {
|
||||
g.to_string()
|
||||
} else {
|
||||
@@ -259,8 +320,15 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|
||||
|| source_spec.starts_with("git@")
|
||||
{
|
||||
git = Some(source_spec);
|
||||
} else if source_spec.starts_with("./")
|
||||
|| source_spec.starts_with("../")
|
||||
|| std::path::Path::new(&source_spec).is_absolute()
|
||||
{
|
||||
path = Some(source_spec);
|
||||
} else {
|
||||
error("Source must have a prefix (path:, git:, github:, gitlab:) or be a full URL");
|
||||
error!(
|
||||
"Source must be a path, pkg-config:, a prefixed source (path:, git:, github:, gitlab:), or a full URL",
|
||||
);
|
||||
return Err(1);
|
||||
}
|
||||
|
||||
@@ -272,5 +340,6 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|
||||
tag,
|
||||
rev,
|
||||
version_from_registry: None,
|
||||
pkg_config,
|
||||
})
|
||||
}
|
||||
|
||||
245
src/cli/build.rs
245
src/cli/build.rs
@@ -15,32 +15,55 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::cli::clean::clean;
|
||||
use crate::cli::flags::parse_build_run_flags;
|
||||
use crate::cli::r#gen::{ProjectInfo, write_dcr_metadata};
|
||||
use crate::core::build::builder::artifact::{absolute_artifact_path, resolve_artifact_path};
|
||||
use crate::core::build::common;
|
||||
use crate::core::build::{BuildEvent, BuildReporter, BuildRequest, run_build};
|
||||
use crate::core::build_config::Config;
|
||||
use crate::utils::fs::find_project_root;
|
||||
use crate::utils::log::error;
|
||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_YELLOW, colored, printc};
|
||||
use crate::utils::build::{
|
||||
get_config_opt, get_config_str, get_language_with_profile_or_default, get_string_with_profile,
|
||||
normalize_kind, resolve_artifact_target_dir, resolve_compiler,
|
||||
};
|
||||
use crate::utils::cli_styles::{
|
||||
BOLD_CYAN, BOLD_YELLOW, Colorize, HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST, SUCCESS_ST, Style,
|
||||
};
|
||||
use crate::utils::fs::{canonicalize_path, find_project_root};
|
||||
use crate::utils::log::sprintln;
|
||||
use std::io::IsTerminal;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
|
||||
pub use crate::core::build::get_build_string_with_profile;
|
||||
|
||||
fn status(verb: &str, style: &str) -> String {
|
||||
colored(&format!("{verb:<9}"), style)
|
||||
/// Formats a left-aligned status verb with the given style for CLI output.
|
||||
fn status(verb: &str, style: Style) -> String {
|
||||
format!("{verb:<9}").style(style).to_string()
|
||||
}
|
||||
|
||||
/// CLI build reporter that prints human-readable progress to stderr/stdout.
|
||||
struct CliReporter;
|
||||
|
||||
impl CliReporter {
|
||||
fn line(&self, verb: &str, style: &str, rest: &str) {
|
||||
/// Prints a single status line with a styled verb and free-form rest text.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `verb`: Short status label (e.g. `"target"`, `"dep"`).
|
||||
/// - `style`: Color/style constant for the verb.
|
||||
/// - `rest`: Remaining message text after the verb.
|
||||
fn line(&self, verb: &str, style: Style, rest: &str) {
|
||||
eprintln!(" {} {}", status(verb, style), rest);
|
||||
}
|
||||
}
|
||||
|
||||
impl BuildReporter for CliReporter {
|
||||
/// Handles build events by printing progress, status lines, and compiler output.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `event`: Build pipeline event to report to the user.
|
||||
fn on_event(&mut self, event: BuildEvent<'_>) {
|
||||
match event {
|
||||
BuildEvent::TargetStart {
|
||||
@@ -73,13 +96,15 @@ impl BuildReporter for CliReporter {
|
||||
rebuilt,
|
||||
} => {
|
||||
common::finish_progress_line();
|
||||
// Only announce deps that were actually rebuilt this run.
|
||||
if rebuilt {
|
||||
self.line("ready", BOLD_GREEN, &format!("{name} v{version}"));
|
||||
self.line("ready", SUCCESS_ST, &format!("{name} v{version}"));
|
||||
}
|
||||
}
|
||||
BuildEvent::Compiling { name, version } => {
|
||||
common::finish_progress_line();
|
||||
let label = format!(" {} {} v{}", status("compile", BOLD_GREEN), name, version);
|
||||
let label = format!(" {} {} v{}", status("compile", SUCCESS_ST), name, version);
|
||||
// Terminals use an in-place progress line; non-TTY gets a plain log line.
|
||||
common::set_progress_label(Some(label.clone()));
|
||||
if !std::io::stderr().is_terminal() {
|
||||
eprintln!("{label}");
|
||||
@@ -91,9 +116,10 @@ impl BuildReporter for CliReporter {
|
||||
}
|
||||
BuildEvent::Finished { secs } => {
|
||||
common::finish_progress_line();
|
||||
self.line("done", BOLD_GREEN, &format!("in {secs}s"));
|
||||
self.line("done", SUCCESS_ST, &format!("in {secs}s"));
|
||||
}
|
||||
BuildEvent::CompilerOutput { stream, text } => {
|
||||
// Pause the progress spinner so compiler text is not overwritten.
|
||||
common::interrupt_progress_for_output();
|
||||
if stream == "stderr" {
|
||||
eprint!("{text}");
|
||||
@@ -105,18 +131,29 @@ impl BuildReporter for CliReporter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the project build command from the CLI.
|
||||
///
|
||||
/// Parses flags, locates the project root, optionally cleans, then invokes the
|
||||
/// core build pipeline. On success, may write `.dcr` metadata and print the
|
||||
/// main artifact path when requested.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `args`: Tokens after `dcr build` (profile, target, force, clean, etc.).
|
||||
///
|
||||
/// # Returns
|
||||
/// Process exit code: `0` on success or help, non-zero on failure.
|
||||
pub fn build(args: &[String]) -> i32 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
printc("USAGE:", BOLD_GREEN);
|
||||
printc(
|
||||
" dcr build [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose]",
|
||||
BOLD_CYAN,
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
|
||||
sprintln!(
|
||||
HELP_EXAMPLES_ST,
|
||||
" dcr build [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [--print-artifact-path]"
|
||||
);
|
||||
println!();
|
||||
printc("DESCRIPTION:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
|
||||
println!(" Compiles the project. Default profile is --debug.");
|
||||
println!();
|
||||
printc("OPTIONS:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "OPTIONS:");
|
||||
println!(" --debug Build with debug profile (default)");
|
||||
println!(" --release Build with release profile");
|
||||
println!(" --target <triple> Cross-compile for the given target");
|
||||
@@ -124,9 +161,13 @@ pub fn build(args: &[String]) -> i32 {
|
||||
println!(" --clean Clean before building");
|
||||
println!(" --verbose Print detailed build output");
|
||||
println!(" --workspace <name> Build a specific workspace member");
|
||||
println!(
|
||||
" --print-artifact-path Print absolute main artifact path on stdout after success"
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Shared cancel flag so Ctrl-C can stop an in-progress build cooperatively.
|
||||
let cancel = Arc::new(AtomicBool::new(false));
|
||||
let cancel_clone = cancel.clone();
|
||||
ctrlc::set_handler(move || {
|
||||
@@ -136,18 +177,18 @@ pub fn build(args: &[String]) -> i32 {
|
||||
let start_dir = match std::env::current_dir() {
|
||||
Ok(dir) => dir,
|
||||
Err(_) => {
|
||||
error("Failed to determine current directory");
|
||||
error!("Failed to determine current directory");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
let root = match find_project_root(&start_dir) {
|
||||
Ok(Some(dir)) => dir,
|
||||
Ok(Some(dir)) => canonicalize_path(&dir),
|
||||
Ok(None) => {
|
||||
error("dcr.toml file not found");
|
||||
error!("dcr.toml file not found");
|
||||
return 1;
|
||||
}
|
||||
Err(_) => {
|
||||
error("Failed to find project root");
|
||||
error!("Failed to find project root");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
@@ -156,6 +197,7 @@ pub fn build(args: &[String]) -> i32 {
|
||||
Err(code) => return code,
|
||||
};
|
||||
|
||||
// Prefer CLI --target; otherwise fall back to profile target from dcr.toml.
|
||||
if flags.target.is_none() {
|
||||
let config_path = root.join("dcr.toml");
|
||||
if let Ok(config) = Config::open(config_path.to_str().unwrap())
|
||||
@@ -174,20 +216,175 @@ pub fn build(args: &[String]) -> i32 {
|
||||
}
|
||||
|
||||
let req = BuildRequest {
|
||||
profile: flags.profile,
|
||||
target: flags.target,
|
||||
profile: flags.profile.clone(),
|
||||
target: flags.target.clone(),
|
||||
force: flags.force,
|
||||
verbose: flags.verbose,
|
||||
workspace: flags.workspace,
|
||||
workspace: flags.workspace.clone(),
|
||||
cancel,
|
||||
};
|
||||
let mut reporter = CliReporter;
|
||||
match run_build(&root, &req, &mut reporter) {
|
||||
Ok(_) => 0,
|
||||
Ok(_) => {
|
||||
// Persist IDE/metadata and optionally echo the main artifact path.
|
||||
if let Some(info) =
|
||||
resolve_post_build_info(&root, &flags.profile, flags.target.as_deref())
|
||||
{
|
||||
let _ = write_dcr_metadata(&root, &info);
|
||||
if flags.print_artifact_path
|
||||
&& let Some(ref path) = info.artifact_path
|
||||
{
|
||||
println!("{path}");
|
||||
}
|
||||
} else if flags.print_artifact_path
|
||||
&& let Some(path) =
|
||||
fallback_artifact_path(&root, &flags.profile, flags.target.as_deref())
|
||||
{
|
||||
println!("{path}");
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(err) => {
|
||||
common::finish_progress_line();
|
||||
error(&err.message);
|
||||
error!("{}", err.message);
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collects project and artifact metadata after a successful build.
|
||||
///
|
||||
/// Reads `dcr.toml`, resolves language/compiler/kind/target/output settings for
|
||||
/// the given profile, and computes absolute artifact and target directory paths.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `root`: Project root with `dcr.toml`.
|
||||
/// - `profile`: Build profile used for the just-finished build.
|
||||
/// - `cli_target`: Optional target from the CLI (overrides config when set).
|
||||
///
|
||||
/// # Returns
|
||||
/// `Some(ProjectInfo)` for a normal package; `None` for workspace-only roots
|
||||
/// or when required config/paths cannot be resolved.
|
||||
fn resolve_post_build_info(
|
||||
root: &Path,
|
||||
profile: &str,
|
||||
cli_target: Option<&str>,
|
||||
) -> Option<ProjectInfo> {
|
||||
let config = Config::open(root.join("dcr.toml").to_str()?).ok()?;
|
||||
if config.is_workspace_only() {
|
||||
return None;
|
||||
}
|
||||
let name = get_config_str(&config, "package.name");
|
||||
let version = get_config_str(&config, "package.version");
|
||||
let language = get_language_with_profile_or_default(&config, profile);
|
||||
let standard = get_string_with_profile(&config, "standard", profile);
|
||||
let cxx_standard = get_string_with_profile(&config, "cxx_standard", profile);
|
||||
let compiler_s = get_string_with_profile(&config, "compiler", profile);
|
||||
let kind = normalize_kind(&get_string_with_profile(&config, "kind", profile)).to_string();
|
||||
let build_target = cli_target
|
||||
.map(|s| s.to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| get_string_with_profile(&config, "target", profile));
|
||||
let output_filename_s = get_string_with_profile(&config, "filename", profile);
|
||||
let output_extension_s = get_string_with_profile(&config, "extension", profile);
|
||||
let output_filename = if output_filename_s.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(output_filename_s)
|
||||
};
|
||||
let output_extension = if output_extension_s.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(output_extension_s)
|
||||
};
|
||||
let tc_cc = get_config_opt(&config, "toolchain.cc");
|
||||
let tc_cxx = get_config_opt(&config, "toolchain.cxx");
|
||||
let tc_as = get_config_opt(&config, "toolchain.as");
|
||||
let resolved_compiler = resolve_compiler(
|
||||
&language,
|
||||
&compiler_s,
|
||||
tc_cc.as_deref(),
|
||||
tc_cxx.as_deref(),
|
||||
tc_as.as_deref(),
|
||||
);
|
||||
let out_dir = get_string_with_profile(&config, "out_dir", profile);
|
||||
// Explicit target (CLI or config) changes how the artifact directory is laid out.
|
||||
let has_explicit = cli_target.is_some_and(|t| !t.is_empty()) || !build_target.trim().is_empty();
|
||||
let target_dir =
|
||||
resolve_artifact_target_dir(root, None, profile, &build_target, &out_dir, has_explicit);
|
||||
let rel = resolve_artifact_path(
|
||||
&kind,
|
||||
profile,
|
||||
&name,
|
||||
Some(&target_dir),
|
||||
output_filename.as_deref(),
|
||||
output_extension.as_deref(),
|
||||
)?;
|
||||
let artifact_path = absolute_artifact_path(root, &rel)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let abs_target_dir = {
|
||||
let p = Path::new(&target_dir);
|
||||
let path = if p.is_absolute() {
|
||||
canonicalize_path(p)
|
||||
} else {
|
||||
canonicalize_path(&root.join(p))
|
||||
};
|
||||
path.to_string_lossy().into_owned()
|
||||
};
|
||||
Some(ProjectInfo {
|
||||
name,
|
||||
version,
|
||||
root: root.to_path_buf(),
|
||||
profile: profile.to_string(),
|
||||
language,
|
||||
standard,
|
||||
cxx_standard,
|
||||
compiler: resolved_compiler,
|
||||
kind: kind.clone(),
|
||||
target: build_target,
|
||||
target_dir: Some(abs_target_dir),
|
||||
artifact_path: Some(artifact_path),
|
||||
artifact_kind: kind,
|
||||
sources: vec![],
|
||||
include_dirs: vec![],
|
||||
lib_dirs: vec![],
|
||||
libs: vec![],
|
||||
cflags: vec![],
|
||||
ldflags: vec![],
|
||||
source_roots: vec![],
|
||||
include_globs: vec![],
|
||||
exclude_globs: vec![],
|
||||
output_filename,
|
||||
output_extension,
|
||||
out_dir,
|
||||
debugger: "lldb".to_string(),
|
||||
moc: None,
|
||||
uic: None,
|
||||
rcc: None,
|
||||
workspace_root: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Best-effort absolute main artifact path when full post-build info is unavailable.
|
||||
///
|
||||
/// Assumes a binary (`"bin"`) kind and uses the package name plus profile,
|
||||
/// target, and `out_dir` settings from config.
|
||||
fn fallback_artifact_path(root: &Path, profile: &str, target: Option<&str>) -> Option<String> {
|
||||
let config = Config::open(root.join("dcr.toml").to_str()?).ok()?;
|
||||
let name = get_config_str(&config, "package.name");
|
||||
if name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let build_target = target.unwrap_or("").to_string();
|
||||
let out_dir = get_string_with_profile(&config, "out_dir", profile);
|
||||
let has_explicit = !build_target.trim().is_empty();
|
||||
let target_dir =
|
||||
resolve_artifact_target_dir(root, None, profile, &build_target, &out_dir, has_explicit);
|
||||
let rel = resolve_artifact_path("bin", profile, &name, Some(&target_dir), None, None)?;
|
||||
Some(
|
||||
absolute_artifact_path(root, &rel)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,28 +15,40 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::core::build_config::Config;
|
||||
use crate::core::workspace::parse_workspace;
|
||||
use crate::utils::build::{default_profile_flags, default_target_triple, parse_version_info};
|
||||
use crate::utils::cli_styles::{
|
||||
BOLD_GREEN, Colorize, HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST, SUCCESS_ST,
|
||||
};
|
||||
use crate::utils::fs::{check_dir, find_project_root, with_dir};
|
||||
use crate::utils::log::{error, warn};
|
||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
|
||||
use crate::utils::log::sprintln;
|
||||
use glob::glob;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::{fs, path::Path};
|
||||
|
||||
/// Entry point for the `dcr clean` command.
|
||||
///
|
||||
/// Removes build artifacts from the project (and optionally workspace members).
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `args`: Tokens after `dcr clean` (`--debug`/`--release`, `--target`, `--all`).
|
||||
///
|
||||
/// # Returns
|
||||
/// Process exit code: `0` on success, `1` on failure.
|
||||
pub fn clean(args: &[String]) -> i32 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
printc("USAGE:", BOLD_GREEN);
|
||||
printc(
|
||||
" dcr clean [--debug | --release] [--target <triple>] [--all]",
|
||||
BOLD_CYAN,
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
|
||||
sprintln!(
|
||||
HELP_EXAMPLES_ST,
|
||||
" dcr clean [--debug | --release] [--target <triple>] [--all]"
|
||||
);
|
||||
println!();
|
||||
printc("DESCRIPTION:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
|
||||
println!(" Removes build artifacts from the target directory.");
|
||||
println!();
|
||||
printc("OPTIONS:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "OPTIONS:");
|
||||
println!(" --debug Clean debug artifacts (default)");
|
||||
println!(" --release Clean release artifacts");
|
||||
println!(" --target <triple> Clean artifacts for a specific target");
|
||||
@@ -47,44 +59,53 @@ pub fn clean(args: &[String]) -> i32 {
|
||||
let start_dir = match std::env::current_dir() {
|
||||
Ok(dir) => dir,
|
||||
Err(_) => {
|
||||
error("Failed to determine current directory");
|
||||
error!("Failed to determine current directory");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
let root = match find_project_root(&start_dir) {
|
||||
Ok(Some(dir)) => dir,
|
||||
Ok(None) => {
|
||||
error("dcr.toml file not found");
|
||||
error!("dcr.toml file not found");
|
||||
return 1;
|
||||
}
|
||||
Err(_) => {
|
||||
error("Failed to find project root");
|
||||
error!("Failed to find project root");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
let flags = match parse_clean_flags(args) {
|
||||
Ok(v) => v,
|
||||
Err(msg) => {
|
||||
error(&msg);
|
||||
error!("{msg}");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
// Run cleanup relative to the project root so paths resolve consistently.
|
||||
match with_dir(&root, || clean_from_root(&root, &flags)) {
|
||||
Ok(()) => 0,
|
||||
Err(msg) => {
|
||||
error(&msg);
|
||||
error!("{msg}");
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed flags controlling which artifacts `clean` should remove.
|
||||
struct CleanFlags {
|
||||
/// Optional build profile name (e.g. `debug`, `release`).
|
||||
profile: Option<String>,
|
||||
/// Optional target triple to clean under `target/<triple>/...`.
|
||||
target: Option<String>,
|
||||
/// When true, also clean each workspace member that has a `target` directory.
|
||||
all: bool,
|
||||
}
|
||||
|
||||
/// Parses CLI arguments into [`CleanFlags`].
|
||||
///
|
||||
/// Recognizes `--all`, `--target <triple>`, and profile flags such as `--debug`
|
||||
/// / `--release` (validated via [`default_profile_flags`]).
|
||||
fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
|
||||
let mut profile: Option<String> = None;
|
||||
let mut target: Option<String> = None;
|
||||
@@ -104,6 +125,7 @@ fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
|
||||
continue;
|
||||
}
|
||||
if arg.starts_with("--") {
|
||||
// Accept only known profile names (e.g. debug/release), not arbitrary flags.
|
||||
let candidate = arg.trim_start_matches("--").to_string();
|
||||
if !default_profile_flags(&candidate).is_empty() {
|
||||
if profile.is_some() {
|
||||
@@ -122,9 +144,14 @@ fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Cleans the project at `root`, and optionally all workspace members.
|
||||
///
|
||||
/// Resolves the effective target triple from flags, `dcr.toml`, or host defaults,
|
||||
/// then removes the appropriate `target` subdirectories and custom clean paths.
|
||||
fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
|
||||
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
|
||||
|
||||
// Prefer CLI --target, then build.target from config, then host defaults on Linux/BSD.
|
||||
let target = flags.target.clone().or_else(|| {
|
||||
config
|
||||
.get("build.target")
|
||||
@@ -165,6 +192,12 @@ fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
|
||||
clean_project_at(root, flags.profile.as_deref(), target.as_deref())
|
||||
}
|
||||
|
||||
/// Removes build artifacts for a single project directory.
|
||||
///
|
||||
/// When `profile` is set, only that profile's directory under `target` (optionally
|
||||
/// scoped by `target` triple) is removed, plus custom paths for that profile.
|
||||
/// When `profile` is `None`, the entire `target` tree is removed and custom paths
|
||||
/// are cleaned for both `debug` and `release`.
|
||||
fn clean_project_at(
|
||||
project_root: &Path,
|
||||
profile: Option<&str>,
|
||||
@@ -180,10 +213,7 @@ fn clean_project_at(
|
||||
if !items.contains(&"dcr.toml".to_string()) {
|
||||
return Err("dcr.toml file not found".to_string());
|
||||
}
|
||||
println!(
|
||||
" Cleaning project `{}`",
|
||||
colored(&project_name, BOLD_GREEN)
|
||||
);
|
||||
println!(" Cleaning project `{}`", project_name.style(BOLD_GREEN));
|
||||
if let Some(profile) = profile {
|
||||
let target_dir = if let Some(t) = target {
|
||||
format!("target/{t}/{profile}")
|
||||
@@ -192,22 +222,23 @@ fn clean_project_at(
|
||||
};
|
||||
let target_items = check_dir(Some("target")).unwrap_or_default();
|
||||
let parent_dir = target.unwrap_or("");
|
||||
// Existence check differs: with a triple, look for target/<triple>; otherwise target/<profile>.
|
||||
let dir_exists = if target.is_some() {
|
||||
target_items.contains(&parent_dir.to_string())
|
||||
} else {
|
||||
target_items.contains(&profile.to_string())
|
||||
};
|
||||
if !dir_exists {
|
||||
warn(&format!("Directory target/{} not found", target_dir));
|
||||
warn!("Directory target/{target_dir} not found");
|
||||
} else {
|
||||
println!(" Profile: {}", colored(profile, BOLD_GREEN));
|
||||
println!(" Profile: {}", profile.style(BOLD_GREEN));
|
||||
if let Some(t) = target {
|
||||
println!(" Target: {}", colored(t, BOLD_GREEN));
|
||||
println!(" Target: {}", t.style(BOLD_GREEN));
|
||||
}
|
||||
let _ = fs::remove_dir_all(&target_dir);
|
||||
println!(
|
||||
"{} Removed directory {}",
|
||||
colored("\n ✔", BOLD_GREEN),
|
||||
"\n ✔".style(SUCCESS_ST),
|
||||
target_dir
|
||||
);
|
||||
}
|
||||
@@ -217,12 +248,9 @@ fn clean_project_at(
|
||||
|
||||
if items.contains(&"target".to_string()) {
|
||||
let _ = fs::remove_dir_all("target");
|
||||
println!(
|
||||
"{} Removed directory target",
|
||||
colored("\n ✔", BOLD_GREEN)
|
||||
);
|
||||
println!("{} Removed directory target", "\n ✔".style(SUCCESS_ST));
|
||||
} else {
|
||||
warn("Directory target not found");
|
||||
warn!("Directory target not found");
|
||||
}
|
||||
clean_custom_paths(&config, "debug")?;
|
||||
clean_custom_paths(&config, "release")?;
|
||||
@@ -230,6 +258,10 @@ fn clean_project_at(
|
||||
})
|
||||
}
|
||||
|
||||
/// Deletes extra paths listed under `build.clean` in `dcr.toml` for a profile.
|
||||
///
|
||||
/// Each pattern may contain `{profile}` and version placeholders, which are
|
||||
/// expanded before matching with `glob`. Matched files and directories are removed.
|
||||
fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> {
|
||||
let patterns = match config.get("build.clean") {
|
||||
Some(v) => v
|
||||
@@ -260,6 +292,10 @@ fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Expands version-related placeholders in a clean path pattern.
|
||||
///
|
||||
/// Reads `package.version` from `config`, parses it, and applies
|
||||
/// [`crate::utils::build::substitute_version_vars`].
|
||||
fn substitute_clean_vars(value: &str, config: &Config) -> String {
|
||||
let version = config
|
||||
.get("package.version")
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::utils::log::{error, warn};
|
||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::utils::cli_styles::{HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST};
|
||||
use crate::utils::log::sprintln;
|
||||
use serde::Deserialize;
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
@@ -27,43 +29,58 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/dexoron/dcr/releases/latest";
|
||||
|
||||
/// Metadata for the latest GitHub release of DCR.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Release {
|
||||
/// Release tag, typically including a leading `v` (e.g. `v1.2.3`).
|
||||
tag_name: String,
|
||||
/// Downloadable assets attached to the release.
|
||||
assets: Vec<ReleaseAsset>,
|
||||
}
|
||||
|
||||
/// A single downloadable asset from a GitHub release.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ReleaseAsset {
|
||||
/// Asset file name as published on GitHub.
|
||||
name: String,
|
||||
/// Direct URL used to download the asset.
|
||||
browser_download_url: String,
|
||||
}
|
||||
|
||||
/// Handles the `dcr --update` flag: checks GitHub for a newer release and
|
||||
/// replaces the running binary when an update is available.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `args`: Remaining tokens after `--update` (only `--help` is accepted).
|
||||
///
|
||||
/// # Returns
|
||||
/// `0` on success / already up to date / pacman-managed refuse; `1` on error.
|
||||
pub fn flag_update(args: &[String]) -> i32 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
printc("USAGE:", BOLD_GREEN);
|
||||
printc(" dcr --update", BOLD_CYAN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
|
||||
sprintln!(HELP_EXAMPLES_ST, " dcr --update");
|
||||
println!();
|
||||
printc("DESCRIPTION:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
|
||||
println!(" Updates DCR to the latest version from GitHub Releases.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if !args.is_empty() {
|
||||
warn("Command does not support additional arguments");
|
||||
warn!("Command does not support additional arguments");
|
||||
return 1;
|
||||
}
|
||||
|
||||
let current_exe = match std::env::current_exe() {
|
||||
Ok(path) => path,
|
||||
Err(_) => {
|
||||
error("Failed to resolve current binary path");
|
||||
error!("Failed to resolve current binary path");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
// Refuse self-update when the binary is owned by pacman/AUR.
|
||||
if let Some(package_name) = pacman_owned_package(¤t_exe) {
|
||||
warn("This dcr binary is managed by pacman/AUR");
|
||||
warn!("This dcr binary is managed by pacman/AUR");
|
||||
println!(
|
||||
"Update via package manager: yay/paru -Syu {package_name} or sudo pacman -Syu {package_name}"
|
||||
);
|
||||
@@ -76,11 +93,12 @@ pub fn flag_update(args: &[String]) -> i32 {
|
||||
let release = match fetch_latest_release() {
|
||||
Ok(release) => release,
|
||||
Err(err) => {
|
||||
error(&format!("Failed to check for updates: {err}"));
|
||||
error!("Failed to check for updates: {err}");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
// Normalize the tag by stripping a leading `v` before comparison.
|
||||
let latest_version = release.tag_name.trim_start_matches('v');
|
||||
if latest_version == current_version {
|
||||
println!("Latest version is already installed: {current_version}");
|
||||
@@ -93,14 +111,14 @@ pub fn flag_update(args: &[String]) -> i32 {
|
||||
.iter()
|
||||
.find(|asset| candidate_names.iter().any(|name| name == &asset.name))
|
||||
else {
|
||||
error(&format!("Binary for target {target} not found"));
|
||||
error!("Binary for target {target} not found");
|
||||
return 1;
|
||||
};
|
||||
|
||||
let bytes = match download_asset(&asset.browser_download_url) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(err) => {
|
||||
error(&format!("Failed to download update: {err}"));
|
||||
error!("Failed to download update: {err}");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
@@ -108,14 +126,15 @@ pub fn flag_update(args: &[String]) -> i32 {
|
||||
let temp_path = temp_binary_path(¤t_exe);
|
||||
|
||||
if fs::write(&temp_path, &bytes).is_err() {
|
||||
error("Failed to write temporary binary");
|
||||
error!("Failed to write temporary binary");
|
||||
return 1;
|
||||
}
|
||||
set_executable_permissions(&temp_path);
|
||||
|
||||
// Replace the running executable in place, then clean up the temp file.
|
||||
if self_replace::self_replace(&temp_path).is_err() {
|
||||
let _ = fs::remove_file(&temp_path);
|
||||
error("Failed to replace current binary");
|
||||
error!("Failed to replace current binary");
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -124,6 +143,7 @@ pub fn flag_update(args: &[String]) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
/// Fetches the latest DCR release metadata from the GitHub Releases API.
|
||||
fn fetch_latest_release() -> Result<Release, String> {
|
||||
let response = ureq::get(LATEST_RELEASE_URL)
|
||||
.set("User-Agent", "dcr-updater")
|
||||
@@ -138,6 +158,7 @@ fn fetch_latest_release() -> Result<Release, String> {
|
||||
.map_err(|_| "GitHub API response has an unexpected format".to_string())
|
||||
}
|
||||
|
||||
/// Downloads a release asset from the given URL into memory.
|
||||
fn download_asset(url: &str) -> Result<Vec<u8>, String> {
|
||||
let response = ureq::get(url)
|
||||
.set("User-Agent", "dcr-updater")
|
||||
@@ -155,9 +176,11 @@ fn download_asset(url: &str) -> Result<Vec<u8>, String> {
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// Builds the list of expected asset file names for the given build target.
|
||||
fn asset_candidates(target: &str) -> Vec<String> {
|
||||
let mut names = vec![format!("dcr-{target}")];
|
||||
|
||||
// Windows releases may publish the binary with an `.exe` suffix.
|
||||
if target.contains("-windows-") || target.ends_with("-windows") {
|
||||
names.push(format!("dcr-{target}.exe"));
|
||||
}
|
||||
@@ -165,6 +188,7 @@ fn asset_candidates(target: &str) -> Vec<String> {
|
||||
names
|
||||
}
|
||||
|
||||
/// Creates a unique temporary path next to the current executable for the download.
|
||||
fn temp_binary_path(current_exe: &Path) -> PathBuf {
|
||||
let stamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
@@ -177,6 +201,7 @@ fn temp_binary_path(current_exe: &Path) -> PathBuf {
|
||||
current_exe.with_extension(extension)
|
||||
}
|
||||
|
||||
/// Sets executable permissions on Unix; no-op on other platforms.
|
||||
fn set_executable_permissions(_path: &Path) {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
@@ -189,6 +214,7 @@ fn set_executable_permissions(_path: &Path) {
|
||||
}
|
||||
}
|
||||
|
||||
/// On Linux, returns the pacman package that owns `path`, if any.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn pacman_owned_package(path: &Path) -> Option<String> {
|
||||
let output = Command::new("pacman").arg("-Qoq").arg(path).output().ok()?;
|
||||
@@ -204,6 +230,7 @@ fn pacman_owned_package(path: &Path) -> Option<String> {
|
||||
Some(package_name)
|
||||
}
|
||||
|
||||
/// Non-Linux stub: package-manager ownership is not checked.
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn pacman_owned_package(_path: &Path) -> Option<String> {
|
||||
None
|
||||
|
||||
104
src/cli/flags.rs
104
src/cli/flags.rs
@@ -15,30 +15,75 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::config::{PROFILE, flags};
|
||||
use crate::utils::log::warn;
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::config::{PROFILE, flags};
|
||||
|
||||
/// Flags shared by the `build` and `run` CLI commands.
|
||||
pub struct BuildRunFlags {
|
||||
/// Build profile name (for example `debug` or `release`).
|
||||
pub profile: String,
|
||||
/// Optional target triple or platform name.
|
||||
pub target: Option<String>,
|
||||
/// Optional workspace member name (`--workspace <name>`).
|
||||
pub workspace: Option<String>,
|
||||
/// Force a rebuild even when artifacts appear up to date.
|
||||
pub force: bool,
|
||||
/// Clean build outputs before building.
|
||||
pub clean: bool,
|
||||
/// Enable verbose logging during the build or run.
|
||||
pub verbose: bool,
|
||||
/// Print the final artifact path to the user.
|
||||
pub print_artifact_path: bool,
|
||||
/// Arguments forwarded to the binary after a `--` separator.
|
||||
pub bin_args: Vec<String>,
|
||||
}
|
||||
|
||||
/// Splits CLI arguments at the first `--` separator.
|
||||
///
|
||||
/// Everything before `--` is treated as DCR flags; everything after is
|
||||
/// forwarded to the built binary as `bin_args`.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `args`: Full argument list for the command (without the program name).
|
||||
///
|
||||
/// # Returns
|
||||
/// `(dcr_args, bin_args)`. If `--` is absent, `bin_args` is empty.
|
||||
pub fn split_double_dash(args: &[String]) -> (&[String], &[String]) {
|
||||
match args.iter().position(|a| a == "--") {
|
||||
Some(i) => (&args[..i], &args[i + 1..]),
|
||||
None => (args, &[]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses build/run flags from a list of CLI arguments.
|
||||
///
|
||||
/// Recognizes boolean flags (`--force`, `--clean`, `--verbose`,
|
||||
/// `--print-artifact-path`), value flags (`--workspace`, `--target`), and
|
||||
/// profile names registered via [`flags`]. Arguments after `--` become
|
||||
/// [`BuildRunFlags::bin_args`].
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `args`: CLI arguments for build/run (may include `--`).
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Ok(BuildRunFlags)` on success.
|
||||
/// - `Err(1)` for unknown flags, missing values, or a duplicate profile.
|
||||
pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
|
||||
let (dcr_args, bin_args_slice) = split_double_dash(args);
|
||||
let mut profile = PROFILE.to_string();
|
||||
let mut target = None;
|
||||
let mut workspace = None;
|
||||
let mut force = false;
|
||||
let mut clean = false;
|
||||
let mut verbose = false;
|
||||
let mut iter = args.iter();
|
||||
let mut print_artifact_path = false;
|
||||
let mut iter = dcr_args.iter();
|
||||
|
||||
while let Some(arg) = iter.next() {
|
||||
// DCR build/run options are long flags only (`--name`).
|
||||
if !arg.starts_with("--") {
|
||||
warn("Unknown argument");
|
||||
warn!("Unknown argument");
|
||||
return Err(1);
|
||||
}
|
||||
let candidate = arg.trim_start_matches("--");
|
||||
@@ -54,11 +99,15 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
|
||||
verbose = true;
|
||||
continue;
|
||||
}
|
||||
if candidate == "print-artifact-path" {
|
||||
print_artifact_path = true;
|
||||
continue;
|
||||
}
|
||||
if candidate == "workspace" {
|
||||
if let Some(w) = iter.next() {
|
||||
workspace = Some(w.clone());
|
||||
} else {
|
||||
warn("--workspace requires a value");
|
||||
warn!("--workspace requires a value");
|
||||
return Err(1);
|
||||
}
|
||||
continue;
|
||||
@@ -67,20 +116,21 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
|
||||
if let Some(t) = iter.next() {
|
||||
target = Some(t.clone());
|
||||
} else {
|
||||
warn("--target requires a value");
|
||||
warn!("--target requires a value");
|
||||
return Err(1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Profile names come from config::flags; only one profile may be set.
|
||||
if flags(candidate).is_some() {
|
||||
if profile != PROFILE {
|
||||
warn("Duplicate profile flag");
|
||||
warn!("Duplicate profile flag");
|
||||
return Err(1);
|
||||
}
|
||||
profile = candidate.to_string();
|
||||
continue;
|
||||
}
|
||||
warn("Unknown build flag");
|
||||
warn!("Unknown build flag");
|
||||
return Err(1);
|
||||
}
|
||||
|
||||
@@ -91,5 +141,43 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
|
||||
force,
|
||||
clean,
|
||||
verbose,
|
||||
print_artifact_path,
|
||||
bin_args: bin_args_slice.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Converts string literals into owned `String` CLI arguments for tests.
|
||||
fn s(args: &[&str]) -> Vec<String> {
|
||||
args.iter().map(|a| a.to_string()).collect()
|
||||
}
|
||||
|
||||
/// Checks that without `--`, the full list stays on the DCR side.
|
||||
#[test]
|
||||
fn split_double_dash_empty_tail() {
|
||||
let args = s(&["--release", "--force"]);
|
||||
let (left, right) = split_double_dash(&args);
|
||||
assert_eq!(left, &s(&["--release", "--force"]));
|
||||
assert!(right.is_empty());
|
||||
}
|
||||
|
||||
/// Checks that arguments after `--` are split into the bin-args slice.
|
||||
#[test]
|
||||
fn split_double_dash_with_bin_args() {
|
||||
let args = s(&["--release", "--", "--test_help", "x"]);
|
||||
let (left, right) = split_double_dash(&args);
|
||||
assert_eq!(left, &s(&["--release"]));
|
||||
assert_eq!(right, &s(&["--test_help", "x"]));
|
||||
}
|
||||
|
||||
/// Checks profile parsing and that bin args after `--` are forwarded.
|
||||
#[test]
|
||||
fn parse_run_flags_forwards_after_double_dash() {
|
||||
let flags = parse_build_run_flags(&s(&["--release", "--", "--test_help"])).unwrap();
|
||||
assert_eq!(flags.profile, "release");
|
||||
assert_eq!(flags.bin_args, s(&["--test_help"]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,22 +15,33 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::utils::log::error;
|
||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::utils::cli_styles::{
|
||||
BOLD_GREEN, Colorize, HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST, SUCCESS_ST,
|
||||
};
|
||||
use crate::utils::log::sprintln;
|
||||
use glob::glob;
|
||||
use std::process::Command;
|
||||
|
||||
/// Formats all C/C++ source files using clang-format.
|
||||
///
|
||||
/// Scans the `src/` and `tests/` directories recursively for files with common
|
||||
/// C/C++ extensions and formats them in place with clang-format.
|
||||
///
|
||||
/// If the first argument is `--help`, prints usage information and returns 0.
|
||||
pub fn fmt(args: &[String]) -> i32 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
printc("USAGE:", BOLD_GREEN);
|
||||
printc(" dcr fmt", BOLD_CYAN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
|
||||
sprintln!(HELP_EXAMPLES_ST, " dcr fmt");
|
||||
println!();
|
||||
printc("DESCRIPTION:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
|
||||
println!(" Formats all C/C++ source files using clang-format.");
|
||||
println!(" Scans src/ and tests/ directories recursively.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Collect all matching files from src/ and tests/ using glob patterns
|
||||
let patterns = [
|
||||
"src/**/*.c",
|
||||
"src/**/*.cpp",
|
||||
@@ -62,13 +73,14 @@ pub fn fmt(args: &[String]) -> i32 {
|
||||
}
|
||||
|
||||
if files.is_empty() {
|
||||
println!(" {} No files to format", colored("Format", BOLD_GREEN));
|
||||
println!(" {} No files to format", "Format".style(BOLD_GREEN));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Format the collected files
|
||||
println!(
|
||||
" {} {} files",
|
||||
colored("Formatting", BOLD_GREEN),
|
||||
"Formatting".style(BOLD_GREEN),
|
||||
files.len()
|
||||
);
|
||||
|
||||
@@ -76,15 +88,15 @@ pub fn fmt(args: &[String]) -> i32 {
|
||||
|
||||
match status {
|
||||
Ok(s) if s.success() => {
|
||||
println!(" {} successful", colored("Format", BOLD_GREEN));
|
||||
println!(" {} successful", "Format".style(SUCCESS_ST));
|
||||
0
|
||||
}
|
||||
Ok(s) => {
|
||||
error(&format!("clang-format failed with status: {}", s));
|
||||
error!("clang-format failed with status: {s}");
|
||||
1
|
||||
}
|
||||
Err(e) => {
|
||||
error(&format!("Failed to execute clang-format: {}", e));
|
||||
error!("Failed to execute clang-format: {e}");
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
753
src/cli/gen.rs
753
src/cli/gen.rs
File diff suppressed because it is too large
Load Diff
@@ -15,16 +15,23 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
|
||||
use crate::utils::cli_styles::{HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST};
|
||||
use crate::utils::log::sprintln;
|
||||
|
||||
/// Displays the help information for the DCR CLI tool.
|
||||
///
|
||||
/// Prints usage, commands, flags, options, examples, and a tip.
|
||||
///
|
||||
/// # Returns
|
||||
/// Always `0`.
|
||||
pub fn help() -> i32 {
|
||||
println!("DCR (Dexoron Cargo Realization)");
|
||||
println!("C project manager inspired by Cargo.");
|
||||
println!();
|
||||
printc("USAGE:", BOLD_GREEN);
|
||||
printc(" dcr <command> [options]", BOLD_CYAN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
|
||||
sprintln!(HELP_EXAMPLES_ST, " dcr <command> [options]");
|
||||
println!();
|
||||
printc("COMMANDS:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "COMMANDS:");
|
||||
println!(" new <name> Create a new project");
|
||||
println!(" init Initialize the current directory as a project");
|
||||
println!(" build [--profile] Build the project (default: --debug)");
|
||||
@@ -37,24 +44,24 @@ pub fn help() -> i32 {
|
||||
println!(" setup Show configured package registries");
|
||||
println!(" clean Remove the target directory");
|
||||
println!(" gen <subcommand> Generate IDE integration files");
|
||||
printc("FLAGS:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "FLAGS:");
|
||||
println!(" --help Show command help");
|
||||
println!(" --update Update dcr to the latest version");
|
||||
println!(" --version Show dcr version");
|
||||
println!();
|
||||
printc("OPTIONS:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "OPTIONS:");
|
||||
println!(" --debug Use debug profile");
|
||||
println!(" --release Use release profile");
|
||||
println!(" --force Force rebuild (build/run)");
|
||||
println!(" --clean Clean before build (build/run)");
|
||||
println!(" --all Clean all workspace members (clean)");
|
||||
println!();
|
||||
printc("EXAMPLES:", BOLD_GREEN);
|
||||
printc(" dcr new hello", BOLD_CYAN);
|
||||
printc(" dcr build --release", BOLD_CYAN);
|
||||
printc(" dcr run --debug", BOLD_CYAN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "EXAMPLES:");
|
||||
sprintln!(HELP_EXAMPLES_ST, " dcr new hello");
|
||||
sprintln!(HELP_EXAMPLES_ST, " dcr build --release");
|
||||
sprintln!(HELP_EXAMPLES_ST, " dcr run --debug");
|
||||
println!();
|
||||
printc("TIP:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "TIP:");
|
||||
println!(" Run 'dcr <command> --help' for command-specific help.");
|
||||
0
|
||||
}
|
||||
|
||||
@@ -15,27 +15,34 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::config::FILE_MAIN_C;
|
||||
use crate::core::build_config::{Config, validate_package_name};
|
||||
use crate::core::vcs::VcsKind;
|
||||
use crate::utils::cli_styles::{
|
||||
BOLD_CYAN, BOLD_GREEN, Colorize, HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST, SUCCESS_ST,
|
||||
};
|
||||
use crate::utils::fs::check_dir;
|
||||
use crate::utils::log::{error, warn};
|
||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use toml::Value;
|
||||
use crate::utils::log::sprintln;
|
||||
use std::{fs, io::Write};
|
||||
|
||||
/// Initializes the current working directory as a new DCR project.
|
||||
///
|
||||
/// Verifies that the current directory is empty, sets up the project structure by creating
|
||||
/// `dcr.toml` and `src/main.c`, and configures version control (Git) integration if applicable.
|
||||
pub fn init(args: &[String]) -> i32 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
printc("USAGE:", BOLD_GREEN);
|
||||
printc(" dcr init [--vcs <git|none>]", BOLD_CYAN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
|
||||
sprintln!(HELP_EXAMPLES_ST, " dcr init [--vcs <git|none>]");
|
||||
println!();
|
||||
printc("DESCRIPTION:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
|
||||
println!(" Initializes the current directory as a DCR project.");
|
||||
println!(" The directory must be empty.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Parse options (--vcs) and collect positional arguments.
|
||||
let mut vcs_str = None;
|
||||
let mut clean_args = Vec::new();
|
||||
let mut iter = args.iter();
|
||||
@@ -44,7 +51,7 @@ pub fn init(args: &[String]) -> i32 {
|
||||
if let Some(val) = iter.next() {
|
||||
vcs_str = Some(val.clone());
|
||||
} else {
|
||||
error("--vcs requires a value");
|
||||
error!("--vcs requires a value");
|
||||
return 1;
|
||||
}
|
||||
} else if let Some(stripped) = arg.strip_prefix("--vcs=") {
|
||||
@@ -55,10 +62,11 @@ pub fn init(args: &[String]) -> i32 {
|
||||
}
|
||||
|
||||
if !clean_args.is_empty() {
|
||||
warn("Command does not support additional arguments");
|
||||
warn!("Command does not support additional arguments");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Resolve target project name from current directory name and enforce empty directory check.
|
||||
let items = check_dir(None).unwrap_or_default();
|
||||
let project_name = std::env::current_dir()
|
||||
.ok()
|
||||
@@ -66,16 +74,15 @@ pub fn init(args: &[String]) -> i32 {
|
||||
.unwrap_or_else(|| "project".to_string());
|
||||
|
||||
if !items.is_empty() {
|
||||
error("Directory not empty");
|
||||
error!("Directory not empty");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if let Err(e) = validate_package_name(&project_name) {
|
||||
error(&format!(
|
||||
"Invalid project name `{}`: {}",
|
||||
colored(&project_name, BOLD_CYAN),
|
||||
e
|
||||
));
|
||||
error!(
|
||||
"Invalid project name `{}`: {e}",
|
||||
project_name.style(BOLD_CYAN),
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -84,54 +91,46 @@ pub fn init(args: &[String]) -> i32 {
|
||||
.unwrap_or_else(|_| ".".to_string());
|
||||
println!("Initializing the project in {cwd}");
|
||||
|
||||
let mut config = match Config::new("./dcr.toml") {
|
||||
Ok(cfg) => cfg,
|
||||
Err(_) => {
|
||||
error("Failed to create dcr.toml");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
if config
|
||||
.edit("package.name", Value::String(project_name.clone()))
|
||||
.is_err()
|
||||
{
|
||||
error("Failed to write dcr.toml");
|
||||
// Create and initialize default manifest (dcr.toml).
|
||||
if Config::create("./dcr.toml", Some(&project_name)).is_err() {
|
||||
error!("Failed to create dcr.toml");
|
||||
return 1;
|
||||
}
|
||||
println!(
|
||||
" {} Created file {}",
|
||||
colored("✔", BOLD_GREEN),
|
||||
colored("dcr.toml", BOLD_CYAN)
|
||||
"✔".style(SUCCESS_ST),
|
||||
"dcr.toml".style(BOLD_CYAN)
|
||||
);
|
||||
|
||||
// Create src directory and write template main.c file.
|
||||
if fs::create_dir("src").is_err() {
|
||||
error("Failed to create src/");
|
||||
error!("Failed to create src/");
|
||||
return 1;
|
||||
}
|
||||
let mut main_c = match fs::File::create("./src/main.c") {
|
||||
Ok(file) => file,
|
||||
Err(_) => {
|
||||
error("Failed to create src/main.c");
|
||||
error!("Failed to create src/main.c");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
if main_c.write_all(FILE_MAIN_C.as_bytes()).is_err() {
|
||||
error("Failed to write src/main.c");
|
||||
error!("Failed to write src/main.c");
|
||||
return 1;
|
||||
}
|
||||
println!(
|
||||
" {} Created file {}",
|
||||
colored("✔", BOLD_GREEN),
|
||||
colored("src/main.c", BOLD_CYAN)
|
||||
"✔".style(SUCCESS_ST),
|
||||
"src/main.c".style(BOLD_CYAN)
|
||||
);
|
||||
|
||||
// settings VCS
|
||||
// Resolve target VCS provider or detect existing repositories.
|
||||
let mut vcs_kind = VcsKind::Git;
|
||||
if let Some(ref vcs_val) = vcs_str {
|
||||
match VcsKind::parse(vcs_val) {
|
||||
Ok(kind) => vcs_kind = kind,
|
||||
Err(e) => {
|
||||
error(&e);
|
||||
error!("{e}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -145,25 +144,26 @@ pub fn init(args: &[String]) -> i32 {
|
||||
if crate::utils::git::is_git_available() {
|
||||
let project_path = std::path::Path::new(".");
|
||||
if let Err(e) = crate::core::vcs::init_vcs(vcs_kind, project_path) {
|
||||
warn(&format!("Failed to initialize git repository: {}", e));
|
||||
warn!("Failed to initialize git repository: {e}");
|
||||
} else {
|
||||
println!(
|
||||
" {} Initialized git repository",
|
||||
colored("✔", BOLD_GREEN)
|
||||
);
|
||||
println!(" {} Initialized git repository", "✔".style(SUCCESS_ST));
|
||||
}
|
||||
} else {
|
||||
warn(
|
||||
warn!(
|
||||
"Git is not installed or not found in PATH. Skipping Git repository initialization.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = crate::utils::fs::ensure_gitignore_has_dcr(std::path::Path::new(".")) {
|
||||
warn!("Failed to update .gitignore: {e}");
|
||||
}
|
||||
|
||||
println!(
|
||||
"Project `{}` successfully created\n",
|
||||
colored(project_name.as_str(), BOLD_GREEN)
|
||||
project_name.style(SUCCESS_ST)
|
||||
);
|
||||
printc("Next step:", BOLD_GREEN);
|
||||
printc(" dcr run", BOLD_CYAN);
|
||||
sprintln!(BOLD_GREEN, "Next step:");
|
||||
sprintln!(BOLD_CYAN, " dcr run");
|
||||
0
|
||||
}
|
||||
|
||||
@@ -15,25 +15,32 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::core::build::common;
|
||||
use crate::core::build_config::Config;
|
||||
use crate::utils::build::{get_bool_with_profile, get_list_with_profile};
|
||||
use crate::utils::cli_styles::{
|
||||
ALERT_ST, BOLD_GREEN, Colorize, HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST, SUCCESS_ST,
|
||||
};
|
||||
use crate::utils::fs::find_project_root;
|
||||
use crate::utils::log::error;
|
||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_YELLOW, colored, printc};
|
||||
use crate::utils::log::sprintln;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
/// 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);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
|
||||
sprintln!(HELP_EXAMPLES_ST, " dcr lint [--fix]");
|
||||
println!();
|
||||
printc("DESCRIPTION:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
|
||||
println!(" Runs clang-tidy on all C/C++ source files.");
|
||||
println!(" Scans source roots (configurable via build.roots) and tests/ directory.");
|
||||
println!();
|
||||
printc("OPTIONS:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "OPTIONS:");
|
||||
println!(" --fix Apply clang-tidy suggestions automatically");
|
||||
return 0;
|
||||
}
|
||||
@@ -43,7 +50,7 @@ pub fn lint(args: &[String]) -> i32 {
|
||||
let start_dir = match std::env::current_dir() {
|
||||
Ok(dir) => dir,
|
||||
Err(_) => {
|
||||
error("Failed to determine current directory");
|
||||
error!("Failed to determine current directory");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
@@ -51,11 +58,11 @@ pub fn lint(args: &[String]) -> i32 {
|
||||
let root = match find_project_root(&start_dir) {
|
||||
Ok(Some(r)) => r,
|
||||
Ok(None) => {
|
||||
error("dcr.toml not found");
|
||||
error!("dcr.toml not found");
|
||||
return 1;
|
||||
}
|
||||
Err(_) => {
|
||||
error("Failed to find project root");
|
||||
error!("Failed to find project root");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
@@ -63,7 +70,7 @@ pub fn lint(args: &[String]) -> i32 {
|
||||
let config = match Config::open(&root.join("dcr.toml").to_string_lossy()) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error(&format!("Failed to load dcr.toml: {e}"));
|
||||
error!("Failed to load dcr.toml: {e}");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
@@ -72,6 +79,7 @@ pub fn lint(args: &[String]) -> i32 {
|
||||
let build_roots = get_list_with_profile(&config, "roots", profile);
|
||||
let src_disable = get_bool_with_profile(&config, "src_disable", profile, false);
|
||||
|
||||
// Construct source roots from config
|
||||
let mut source_roots: Vec<String> = Vec::new();
|
||||
for raw in &build_roots {
|
||||
let p = Path::new(raw);
|
||||
@@ -94,7 +102,7 @@ pub fn lint(args: &[String]) -> i32 {
|
||||
match common::collect_sources(&roots, &cxx_extensions, &exclude_dirs, &include_paths) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
error(&format!("Failed to collect source files: {e}"));
|
||||
error!("Failed to collect source files: {e}");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
@@ -112,13 +120,13 @@ pub fn lint(args: &[String]) -> i32 {
|
||||
files.sort();
|
||||
|
||||
if files.is_empty() {
|
||||
println!(" {} No files to lint", colored("Lint", BOLD_GREEN));
|
||||
println!(" {} No files to lint", "Lint".style(BOLD_GREEN));
|
||||
return 0;
|
||||
}
|
||||
|
||||
println!(
|
||||
" {} {} files {}",
|
||||
colored("Linting", BOLD_GREEN),
|
||||
"Linting".style(BOLD_GREEN),
|
||||
files.len(),
|
||||
if do_fix { "(with --fix)" } else { "" }
|
||||
);
|
||||
@@ -126,6 +134,7 @@ pub fn lint(args: &[String]) -> i32 {
|
||||
let mut total_issues = 0u32;
|
||||
let mut failed = 0u32;
|
||||
|
||||
// Execute clang-tidy for each file and count issues
|
||||
for file in &files {
|
||||
let mut cmd = Command::new("clang-tidy");
|
||||
if do_fix {
|
||||
@@ -137,7 +146,7 @@ pub fn lint(args: &[String]) -> i32 {
|
||||
let output = match cmd.output() {
|
||||
Ok(o) => o,
|
||||
Err(e) => {
|
||||
error(&format!("Failed to execute clang-tidy: {}", e));
|
||||
error!("Failed to execute clang-tidy: {e}");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
@@ -155,13 +164,13 @@ pub fn lint(args: &[String]) -> i32 {
|
||||
if issue_count > 0 {
|
||||
total_issues += issue_count as u32;
|
||||
failed += 1;
|
||||
print!(" {} ", colored("×", BOLD_YELLOW));
|
||||
print!(" {} ", "×".style(ALERT_ST));
|
||||
println!("{} ({} issues)", file, issue_count);
|
||||
for issue in &issues {
|
||||
println!(" {}", issue);
|
||||
}
|
||||
} else {
|
||||
print!(" {} ", colored("✓", BOLD_GREEN));
|
||||
print!(" {} ", "✓".style(SUCCESS_ST));
|
||||
println!("{}", file);
|
||||
}
|
||||
}
|
||||
@@ -172,13 +181,13 @@ pub fn lint(args: &[String]) -> i32 {
|
||||
if failed == 0 {
|
||||
println!(
|
||||
" {} {} files, no issues",
|
||||
colored("Lint", BOLD_GREEN),
|
||||
"Lint".style(SUCCESS_ST),
|
||||
total
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
" {} {}/{} files passed, {} issues found",
|
||||
colored("Lint", BOLD_YELLOW),
|
||||
"Lint".style(ALERT_ST),
|
||||
passed,
|
||||
total,
|
||||
total_issues
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/// DCR CLI module.
|
||||
///
|
||||
/// This module declares submodules for all DCR commands.
|
||||
pub mod add;
|
||||
pub mod build;
|
||||
pub mod clean;
|
||||
|
||||
112
src/cli/new.rs
112
src/cli/new.rs
@@ -15,22 +15,28 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::config::FILE_MAIN_C;
|
||||
use crate::core::build_config::{Config, validate_package_name};
|
||||
use crate::core::vcs::VcsKind;
|
||||
use crate::utils::cli_styles::{
|
||||
BOLD_CYAN, BOLD_GREEN, Colorize, HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST, SUCCESS_ST,
|
||||
};
|
||||
use crate::utils::fs::check_dir;
|
||||
use crate::utils::log::{error, warn};
|
||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use toml::Value;
|
||||
use crate::utils::log::sprintln;
|
||||
use std::{fs, io::Write};
|
||||
|
||||
/// Executes the `dcr new` command to scaffold a new C/C++ project directory structure.
|
||||
///
|
||||
/// Handles command-line argument parsing, directory creation, default configuration generation (`dcr.toml`),
|
||||
/// template source file generation (`src/main.c`), and initial VCS setup.
|
||||
pub fn new(args: &[String]) -> i32 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
printc("USAGE:", BOLD_GREEN);
|
||||
printc(" dcr new <name> [--vcs <git|none>]", BOLD_CYAN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
|
||||
sprintln!(HELP_EXAMPLES_ST, " dcr new <name> [--vcs <git|none>]");
|
||||
println!();
|
||||
printc("DESCRIPTION:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
|
||||
println!(" Creates a new C/C++ project with the given name.");
|
||||
println!(" The name may only contain ASCII letters, digits, '_' and '-'.");
|
||||
return 0;
|
||||
@@ -38,6 +44,7 @@ pub fn new(args: &[String]) -> i32 {
|
||||
|
||||
let items = check_dir(None).unwrap_or_default();
|
||||
|
||||
// Parse CLI options (--vcs) while extracting positional arguments for the project name.
|
||||
let mut vcs_str = None;
|
||||
let mut clean_args = Vec::new();
|
||||
let mut iter = args.iter();
|
||||
@@ -46,7 +53,7 @@ pub fn new(args: &[String]) -> i32 {
|
||||
if let Some(val) = iter.next() {
|
||||
vcs_str = Some(val.clone());
|
||||
} else {
|
||||
error("--vcs requires a value");
|
||||
error!("--vcs requires a value");
|
||||
return 1;
|
||||
}
|
||||
} else if let Some(stripped) = arg.strip_prefix("--vcs=") {
|
||||
@@ -57,102 +64,91 @@ pub fn new(args: &[String]) -> i32 {
|
||||
}
|
||||
|
||||
if clean_args.is_empty() {
|
||||
error("Project name not specified");
|
||||
error!("Project name not specified");
|
||||
return 1;
|
||||
}
|
||||
if clean_args.len() > 1 {
|
||||
warn("Command does not support additional arguments");
|
||||
warn!("Command does not support additional arguments");
|
||||
return 1;
|
||||
}
|
||||
|
||||
let project_name = &clean_args[0];
|
||||
println!(
|
||||
"Creating a Project `{}`...",
|
||||
colored(project_name, BOLD_CYAN)
|
||||
);
|
||||
println!("Creating a Project `{}`...", project_name.style(BOLD_CYAN));
|
||||
|
||||
if let Err(e) = validate_package_name(project_name) {
|
||||
error(&format!(
|
||||
"Invalid project name `{}`: {}",
|
||||
colored(project_name, BOLD_CYAN),
|
||||
e
|
||||
));
|
||||
error!(
|
||||
"Invalid project name `{}`: {e}",
|
||||
project_name.style(BOLD_CYAN),
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if items.contains(project_name) {
|
||||
error(&format!(
|
||||
error!(
|
||||
"Directory `{}` already exists\n",
|
||||
colored(project_name, BOLD_CYAN)
|
||||
));
|
||||
printc("Hint:", BOLD_CYAN);
|
||||
project_name.style(BOLD_CYAN)
|
||||
);
|
||||
sprintln!(BOLD_CYAN, "Hint:");
|
||||
println!(
|
||||
" Use `{}` to initialize an existing project\n or specify a different project name",
|
||||
colored("dcr init", BOLD_CYAN)
|
||||
"dcr init".style(BOLD_CYAN)
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if fs::create_dir(project_name).is_err() {
|
||||
error("Failed to create directory");
|
||||
error!("Failed to create directory");
|
||||
return 1;
|
||||
}
|
||||
println!(
|
||||
" {} Directory created {}",
|
||||
colored("✔", BOLD_GREEN),
|
||||
"✔".style(SUCCESS_ST),
|
||||
project_name
|
||||
);
|
||||
|
||||
// Initialize and write project configuration manifest (dcr.toml).
|
||||
let toml_path = format!("./{project_name}/dcr.toml");
|
||||
let mut config = match Config::new(&toml_path) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(_) => {
|
||||
error("Failed to create dcr.toml");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
if config
|
||||
.edit("package.name", Value::String(project_name.to_string()))
|
||||
.is_err()
|
||||
{
|
||||
error("Failed to write dcr.toml");
|
||||
if Config::create(&toml_path, Some(project_name)).is_err() {
|
||||
error!("Failed to create dcr.toml");
|
||||
return 1;
|
||||
}
|
||||
println!(
|
||||
" {} Created file {}",
|
||||
colored("✔", BOLD_GREEN),
|
||||
colored("dcr.toml", BOLD_CYAN)
|
||||
"✔".style(SUCCESS_ST),
|
||||
"dcr.toml".style(BOLD_CYAN)
|
||||
);
|
||||
|
||||
// Generate src directory and populate initial main.c file.
|
||||
if fs::create_dir_all(format!("./{project_name}/src")).is_err() {
|
||||
error("Failed to create src/");
|
||||
error!("Failed to create src/");
|
||||
return 1;
|
||||
}
|
||||
let main_c_path = format!("./{project_name}/src/main.c");
|
||||
let mut main_c = match fs::File::create(&main_c_path) {
|
||||
Ok(file) => file,
|
||||
Err(_) => {
|
||||
error("Failed to create src/main.c");
|
||||
error!("Failed to create src/main.c");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
if main_c.write_all(FILE_MAIN_C.as_bytes()).is_err() {
|
||||
error("Failed to write src/main.c");
|
||||
error!("Failed to write src/main.c");
|
||||
return 1;
|
||||
}
|
||||
println!(
|
||||
" {} Created file {}",
|
||||
colored("✔", BOLD_GREEN),
|
||||
colored("src/main.c", BOLD_CYAN)
|
||||
"✔".style(SUCCESS_ST),
|
||||
"src/main.c".style(BOLD_CYAN)
|
||||
);
|
||||
|
||||
// settings VCS
|
||||
// Resolve target VCS mode based on CLI flags or parent directory state.
|
||||
let mut vcs_kind = VcsKind::Git;
|
||||
if let Some(ref vcs_val) = vcs_str {
|
||||
match VcsKind::parse(vcs_val) {
|
||||
Ok(kind) => vcs_kind = kind,
|
||||
Err(e) => {
|
||||
error(&e);
|
||||
error!("{e}");
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -166,25 +162,27 @@ pub fn new(args: &[String]) -> i32 {
|
||||
if crate::utils::git::is_git_available() {
|
||||
let project_path = std::path::Path::new(project_name);
|
||||
if let Err(e) = crate::core::vcs::init_vcs(vcs_kind, project_path) {
|
||||
warn(&format!("Failed to initialize git repository: {}", e));
|
||||
warn!("Failed to initialize git repository: {e}");
|
||||
} else {
|
||||
println!(
|
||||
" {} Initialized git repository",
|
||||
colored("✔", BOLD_GREEN)
|
||||
);
|
||||
println!(" {} Initialized git repository", "✔".style(SUCCESS_ST));
|
||||
}
|
||||
} else {
|
||||
warn(
|
||||
warn!(
|
||||
"Git is not installed or not found in PATH. Skipping Git repository initialization.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let project_path = std::path::Path::new(project_name);
|
||||
if let Err(e) = crate::utils::fs::ensure_gitignore_has_dcr(project_path) {
|
||||
warn!("Failed to update .gitignore: {e}");
|
||||
}
|
||||
|
||||
println!(
|
||||
"Project `{}` successfully created\n",
|
||||
colored(project_name, BOLD_GREEN)
|
||||
project_name.style(SUCCESS_ST)
|
||||
);
|
||||
printc("Next step:", BOLD_GREEN);
|
||||
printc(&format!(" cd {}\n dcr run", project_name), BOLD_CYAN);
|
||||
sprintln!(BOLD_GREEN, "Next step:");
|
||||
sprintln!(BOLD_CYAN, " cd {}\n dcr run", project_name);
|
||||
0
|
||||
}
|
||||
|
||||
171
src/cli/run.rs
171
src/cli/run.rs
@@ -15,18 +15,20 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::cli::build::build;
|
||||
use crate::cli::flags::parse_build_run_flags;
|
||||
use crate::core::build_config::Config;
|
||||
use crate::core::runner::run_binary;
|
||||
use crate::utils::build::{normalize_target_os, parse_version_info, substitute_vars};
|
||||
use crate::utils::fs::find_project_root;
|
||||
use crate::utils::fs::with_dir;
|
||||
use crate::utils::log::error;
|
||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use crate::utils::cli_styles::{BOLD_GREEN, Colorize, HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST};
|
||||
use crate::utils::fs::{find_project_root, with_dir};
|
||||
use crate::utils::log::sprintln;
|
||||
use std::{path::Path, process::Command};
|
||||
|
||||
/// Retrieves the run command from the config, preferring target-specific,
|
||||
/// then profile-specific, then the base `run.cmd`.
|
||||
fn get_run_cmd(
|
||||
config: &Config,
|
||||
profile: &str,
|
||||
@@ -55,42 +57,52 @@ fn get_run_cmd(
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles the `dcr run` subcommand: parses flags, finds the project root,
|
||||
/// builds if needed, and runs the resulting binary or configured command.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `args`: Tokens after `dcr run` (build flags; after `--` → binary args).
|
||||
///
|
||||
/// # Returns
|
||||
/// Process exit code from the built binary / `run.cmd`, or non-zero on build/setup failure.
|
||||
pub fn run(args: &[String]) -> i32 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
printc("USAGE:", BOLD_GREEN);
|
||||
printc(
|
||||
" dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose]",
|
||||
BOLD_CYAN,
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
|
||||
sprintln!(
|
||||
HELP_EXAMPLES_ST,
|
||||
" dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [-- <args>...]"
|
||||
);
|
||||
println!();
|
||||
printc("DESCRIPTION:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
|
||||
println!(" Builds and runs the project. Only available for kind = \"bin\".");
|
||||
println!(" Arguments after `--` are passed to the built binary (cargo-style).");
|
||||
println!();
|
||||
printc("OPTIONS:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "OPTIONS:");
|
||||
println!(" --debug Run with debug profile (default)");
|
||||
println!(" --release Run with release profile");
|
||||
println!(" --target <triple> Cross-compile for the given target");
|
||||
println!(" --force Force a full rebuild");
|
||||
println!(" --clean Clean before building");
|
||||
println!(" --verbose Print detailed build output");
|
||||
println!(" -- <args>... Arguments forwarded to the binary");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let start_dir = match std::env::current_dir() {
|
||||
Ok(dir) => dir,
|
||||
Err(_) => {
|
||||
error("Failed to determine current directory");
|
||||
error!("Failed to determine current directory");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
let root = match find_project_root(&start_dir) {
|
||||
Ok(Some(dir)) => dir,
|
||||
Ok(None) => {
|
||||
error("dcr.toml file not found");
|
||||
error!("dcr.toml file not found");
|
||||
return 1;
|
||||
}
|
||||
Err(_) => {
|
||||
error("Failed to find project root");
|
||||
error!("Failed to find project root");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
@@ -99,7 +111,8 @@ pub fn run(args: &[String]) -> i32 {
|
||||
}) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(err) => {
|
||||
error(&err);
|
||||
error!("{err}");
|
||||
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
@@ -112,12 +125,13 @@ pub fn run(args: &[String]) -> i32 {
|
||||
if let Some(cmd) = get_run_cmd(&config, &flags.profile, flags.target.as_deref(), "") {
|
||||
let build_status = build(&args_for_build(&flags));
|
||||
if build_status == 0 {
|
||||
let display = display_run_cmd(&cmd, &flags.bin_args);
|
||||
println!(
|
||||
" {} {}",
|
||||
colored(&format!("{:<9}", "run"), BOLD_GREEN),
|
||||
cmd
|
||||
format!("{:<9}", "run").style(BOLD_GREEN),
|
||||
display
|
||||
);
|
||||
return run_shell(&cmd);
|
||||
return run_shell_with_args(&cmd, &flags.bin_args);
|
||||
}
|
||||
return build_status;
|
||||
}
|
||||
@@ -130,11 +144,12 @@ pub fn run(args: &[String]) -> i32 {
|
||||
) {
|
||||
Ok(Some(ws)) => ws,
|
||||
Ok(None) => {
|
||||
error("Workspace root has no members defined");
|
||||
error!("Workspace root has no members defined");
|
||||
return 1;
|
||||
}
|
||||
Err(e) => {
|
||||
error(&e);
|
||||
error!("{e}");
|
||||
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
@@ -146,9 +161,9 @@ pub fn run(args: &[String]) -> i32 {
|
||||
Some(m) => m,
|
||||
None => {
|
||||
if let Some(name) = &flags.workspace {
|
||||
error(&format!("Workspace member '{name}' not found"));
|
||||
error!("Workspace member '{name}' not found");
|
||||
} else {
|
||||
error("No workspace member to run (set `main = true` on one member)");
|
||||
error!("No workspace member to run (set `main = true` on one member)");
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
@@ -159,7 +174,8 @@ pub fn run(args: &[String]) -> i32 {
|
||||
}) {
|
||||
Ok(code) => code,
|
||||
Err(e) => {
|
||||
error(&e);
|
||||
error!("{e}");
|
||||
|
||||
1
|
||||
}
|
||||
};
|
||||
@@ -168,12 +184,13 @@ pub fn run(args: &[String]) -> i32 {
|
||||
match run_project(&root, &flags, None) {
|
||||
Ok(code) => code,
|
||||
Err(e) => {
|
||||
error(&e);
|
||||
error!("{e}");
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds and runs a single project at `root` (optionally as a workspace member).
|
||||
fn run_project(
|
||||
root: &Path,
|
||||
flags: &crate::cli::flags::BuildRunFlags,
|
||||
@@ -204,19 +221,17 @@ fn run_project(
|
||||
|
||||
let out_dir =
|
||||
crate::cli::build::get_build_string_with_profile(&config, "out_dir", &flags.profile);
|
||||
let normalized_target_dir = if !out_dir.is_empty() {
|
||||
Some(out_dir)
|
||||
} else {
|
||||
match workspace_root {
|
||||
Some(wr) => target
|
||||
.as_ref()
|
||||
.and_then(|t| crate::utils::build::normalize_target(t, &flags.profile))
|
||||
.map(|rel| wr.join(&rel).to_string_lossy().to_string()),
|
||||
None => target
|
||||
.as_ref()
|
||||
.and_then(|t| crate::utils::build::normalize_target(t, &flags.profile)),
|
||||
}
|
||||
};
|
||||
let build_target_str = target.clone().unwrap_or_default();
|
||||
let has_explicit = target.as_ref().is_some_and(|t| !t.trim().is_empty());
|
||||
|
||||
let normalized_target_dir = Some(crate::utils::build::resolve_artifact_target_dir(
|
||||
root,
|
||||
workspace_root,
|
||||
&flags.profile,
|
||||
&build_target_str,
|
||||
&out_dir,
|
||||
has_explicit,
|
||||
));
|
||||
|
||||
let version = config
|
||||
.get("package.version")
|
||||
@@ -250,32 +265,36 @@ fn run_project(
|
||||
);
|
||||
if build_status == 0 {
|
||||
if let Some(cmd) = run_cmd {
|
||||
let display = display_run_cmd(&cmd, &flags.bin_args);
|
||||
println!(
|
||||
" {} {}",
|
||||
colored(&format!("{:<9}", "run"), BOLD_GREEN),
|
||||
cmd
|
||||
format!("{:<9}", "run").style(BOLD_GREEN),
|
||||
display
|
||||
);
|
||||
return Ok(run_shell(&cmd));
|
||||
return Ok(run_shell_with_args(&cmd, &flags.bin_args));
|
||||
}
|
||||
let display = display_bin_run(&bin_path, &flags.bin_args);
|
||||
println!(
|
||||
" {} {}",
|
||||
colored(&format!("{:<9}", "run"), BOLD_GREEN),
|
||||
bin_path
|
||||
format!("{:<9}", "run").style(BOLD_GREEN),
|
||||
display
|
||||
);
|
||||
return Ok(run_binary(
|
||||
project_name,
|
||||
&flags.profile,
|
||||
normalized_target_dir.as_deref(),
|
||||
&flags.bin_args,
|
||||
));
|
||||
}
|
||||
|
||||
let fallback_code = if let Some(cmd) = run_cmd {
|
||||
run_shell(&cmd)
|
||||
run_shell_with_args(&cmd, &flags.bin_args)
|
||||
} else {
|
||||
run_binary(
|
||||
project_name,
|
||||
&flags.profile,
|
||||
normalized_target_dir.as_deref(),
|
||||
&flags.bin_args,
|
||||
)
|
||||
};
|
||||
if fallback_code != 1 {
|
||||
@@ -285,6 +304,7 @@ fn run_project(
|
||||
Err("Fix errors in the code to run the project".to_string())
|
||||
}
|
||||
|
||||
/// Builds the list of arguments for the build command based on the provided flags.
|
||||
fn args_for_build(flags: &crate::cli::flags::BuildRunFlags) -> Vec<String> {
|
||||
let mut args = Vec::new();
|
||||
args.push(format!("--{}", flags.profile));
|
||||
@@ -308,6 +328,7 @@ fn args_for_build(flags: &crate::cli::flags::BuildRunFlags) -> Vec<String> {
|
||||
args
|
||||
}
|
||||
|
||||
/// Constructs build arguments specifically for a workspace member.
|
||||
fn args_for_workspace_build(
|
||||
flags: &crate::cli::flags::BuildRunFlags,
|
||||
member_name: &str,
|
||||
@@ -332,6 +353,7 @@ fn args_for_workspace_build(
|
||||
args
|
||||
}
|
||||
|
||||
/// Executes the given shell command and returns its exit code.
|
||||
fn run_shell(cmd: &str) -> i32 {
|
||||
let status = if cfg!(target_os = "windows") {
|
||||
Command::new("cmd").arg("/C").arg(cmd).status()
|
||||
@@ -345,6 +367,67 @@ fn run_shell(cmd: &str) -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the command, appending escaped binary arguments if provided.
|
||||
fn run_shell_with_args(cmd: &str, bin_args: &[String]) -> i32 {
|
||||
if bin_args.is_empty() {
|
||||
return run_shell(cmd);
|
||||
}
|
||||
let mut full = cmd.to_string();
|
||||
for arg in bin_args {
|
||||
full.push(' ');
|
||||
full.push_str(&shell_escape(arg));
|
||||
}
|
||||
run_shell(&full)
|
||||
}
|
||||
|
||||
/// Formats the run command string for console output, appending arguments if any.
|
||||
fn display_run_cmd(cmd: &str, bin_args: &[String]) -> String {
|
||||
if bin_args.is_empty() {
|
||||
return cmd.to_string();
|
||||
}
|
||||
let mut display = cmd.to_string();
|
||||
for arg in bin_args {
|
||||
display.push(' ');
|
||||
display.push_str(arg);
|
||||
}
|
||||
display
|
||||
}
|
||||
|
||||
/// Formats the binary path for console display, appending arguments if any.
|
||||
fn display_bin_run(bin_path: &str, bin_args: &[String]) -> String {
|
||||
if bin_args.is_empty() {
|
||||
return bin_path.to_string();
|
||||
}
|
||||
let mut display = bin_path.to_string();
|
||||
for arg in bin_args {
|
||||
display.push(' ');
|
||||
display.push_str(arg);
|
||||
}
|
||||
display
|
||||
}
|
||||
|
||||
/// Escapes the argument for safe shell execution, using double quotes on Windows and single quotes on Unix.
|
||||
fn shell_escape(arg: &str) -> String {
|
||||
if cfg!(target_os = "windows") {
|
||||
// cmd.exe: wrap in double quotes and escape embedded quotes.
|
||||
let escaped = arg.replace('"', "\\\"");
|
||||
format!("\"{escaped}\"")
|
||||
} else {
|
||||
// POSIX sh: single-quote and escape embedded single quotes as '\''.
|
||||
let mut out = String::from("'");
|
||||
for ch in arg.chars() {
|
||||
if ch == '\'' {
|
||||
out.push_str("'\\''");
|
||||
} else {
|
||||
out.push(ch);
|
||||
}
|
||||
}
|
||||
out.push('\'');
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces version info and profile in the command template.
|
||||
fn substitute_run_vars(cmd: &str, profile: &str, version: &str) -> String {
|
||||
let info = parse_version_info(version);
|
||||
substitute_vars(cmd, &info, profile, "")
|
||||
|
||||
@@ -15,20 +15,23 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
|
||||
use crate::utils::cli_styles::{HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST};
|
||||
use crate::utils::log::sprintln;
|
||||
|
||||
/// Sets up DCR registries by loading the registry manager and printing the list of registries.
|
||||
pub fn setup(args: &[String]) -> i32 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
printc("USAGE:", BOLD_GREEN);
|
||||
printc(" dcr setup", BOLD_CYAN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
|
||||
sprintln!(HELP_EXAMPLES_ST, " dcr setup");
|
||||
println!();
|
||||
printc("DESCRIPTION:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
|
||||
println!(" Sets up DCR registries. Downloads and indexes");
|
||||
println!(" package registries for dependency resolution.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
println!("Setting up DCR registries...");
|
||||
// Load and process the registries, returning 0 on success or 1 on error
|
||||
match crate::core::registry::RegistryManager::load() {
|
||||
Ok(manager) => {
|
||||
println!("Loaded {} registries.", manager.config.registry.len());
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::cli::build;
|
||||
use crate::config::{FILE_DCR_TEST_H, FILE_TEST_C, flags};
|
||||
use crate::core::build_config::Config;
|
||||
@@ -22,30 +24,35 @@ use crate::utils::build::{
|
||||
get_config_opt, get_config_str, get_language_with_profile, get_list_with_profile,
|
||||
get_string_with_profile, resolve_compiler, resolve_pkg_config_flags,
|
||||
};
|
||||
use crate::utils::cli_styles::{
|
||||
BOLD_GREEN, Colorize, ERROR_ST, HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST, SKIP_ST, SUCCESS_ST,
|
||||
Style,
|
||||
};
|
||||
use crate::utils::fs::{find_project_root, with_dir};
|
||||
use crate::utils::log::error;
|
||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_RED, RESET, colored, printc};
|
||||
use crate::utils::log::sprintln;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
const BOLD_BLUE: &str = "\x1b[1m\x1b[94m";
|
||||
|
||||
/// Runs project tests and prints a unified testsuite report.
|
||||
pub fn test(args: &[String]) -> i32 {
|
||||
let mut init_header = false;
|
||||
let mut profile = "debug";
|
||||
for arg in args {
|
||||
if arg == "--help" {
|
||||
printc("USAGE:", BOLD_GREEN);
|
||||
printc(" dcr test [--init] [--debug | --release]", BOLD_CYAN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
|
||||
sprintln!(
|
||||
HELP_EXAMPLES_ST,
|
||||
" dcr test [--init] [--debug | --release]"
|
||||
);
|
||||
println!();
|
||||
printc("ALIASES:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "ALIASES:");
|
||||
println!(" dcr tests");
|
||||
println!();
|
||||
printc("DESCRIPTION:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
|
||||
println!(" Runs project tests and prints a unified testsuite report.");
|
||||
println!();
|
||||
printc("OPTIONS:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "OPTIONS:");
|
||||
println!(" --init Create tests/dcr_test.h in current project");
|
||||
println!(" --debug Build and run tests with debug profile (default)");
|
||||
println!(" --release Build and run tests with release profile");
|
||||
@@ -59,25 +66,25 @@ pub fn test(args: &[String]) -> i32 {
|
||||
profile = arg.trim_start_matches("--");
|
||||
continue;
|
||||
}
|
||||
error("Unknown argument");
|
||||
error!("Unknown argument");
|
||||
return 1;
|
||||
}
|
||||
|
||||
let start_dir = match std::env::current_dir() {
|
||||
Ok(dir) => dir,
|
||||
Err(_) => {
|
||||
error("Failed to determine current directory");
|
||||
error!("Failed to determine current directory");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
let root = match find_project_root(&start_dir) {
|
||||
Ok(Some(dir)) => dir,
|
||||
Ok(None) => {
|
||||
error("dcr.toml file not found");
|
||||
error!("dcr.toml file not found");
|
||||
return 1;
|
||||
}
|
||||
Err(_) => {
|
||||
error("Failed to find project root");
|
||||
error!("Failed to find project root");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
@@ -86,7 +93,8 @@ pub fn test(args: &[String]) -> i32 {
|
||||
match with_dir(&root, ensure_test_header) {
|
||||
Ok(()) => {}
|
||||
Err(msg) => {
|
||||
error(&msg);
|
||||
error!("{msg}");
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -95,12 +103,13 @@ pub fn test(args: &[String]) -> i32 {
|
||||
match with_dir(&root, || run_testsuite(profile)) {
|
||||
Ok(code) => code,
|
||||
Err(msg) => {
|
||||
error(&msg);
|
||||
error!("{msg}");
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes the testsuite for the given build profile.
|
||||
fn run_testsuite(profile: &str) -> Result<i32, String> {
|
||||
if build::build(&[format!("--{profile}")]) != 0 {
|
||||
return Ok(1);
|
||||
@@ -132,8 +141,12 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
|
||||
cflags.extend(get_list_with_profile(&config, "cflags", profile));
|
||||
let ldflags = get_list_with_profile(&config, "ldflags", profile);
|
||||
let pkg_configs = get_list_with_profile(&config, "pkg_config", profile);
|
||||
let (cflags, ldflags) = resolve_pkg_config_flags(&pkg_configs, &cflags, &ldflags)?;
|
||||
let includes = test_include_dirs(&config, profile);
|
||||
let (mut cflags, mut ldflags) = resolve_pkg_config_flags(&pkg_configs, &cflags, &ldflags)?;
|
||||
let deps = crate::core::deps::resolve_deps(&config, profile, None, Path::new("."))?;
|
||||
cflags.extend(deps.cflags);
|
||||
ldflags.extend(deps.ldflags);
|
||||
let mut includes = test_include_dirs(&config, profile);
|
||||
includes.extend(deps.include_dirs);
|
||||
let test_sources = collect_test_sources()?;
|
||||
if test_sources.is_empty() {
|
||||
return Err("tests/test.c not found; run 'dcr test --init' first".to_string());
|
||||
@@ -187,19 +200,19 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
|
||||
for line in stdout.lines() {
|
||||
let line = line.trim();
|
||||
if let Some(name) = line.strip_prefix("[PASS] ") {
|
||||
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
|
||||
println!("{} {}", "[PASS]".style(SUCCESS_ST), name);
|
||||
pass += 1;
|
||||
parsed_any = true;
|
||||
continue;
|
||||
}
|
||||
if let Some(name) = line.strip_prefix("[SKIP] ") {
|
||||
println!("{} {}", colored("[SKIP]", BOLD_BLUE), name);
|
||||
println!("{} {}", "[SKIP]".style(SKIP_ST), name);
|
||||
skip += 1;
|
||||
parsed_any = true;
|
||||
continue;
|
||||
}
|
||||
if let Some(name) = line.strip_prefix("[FAIL] ") {
|
||||
println!("{} {}", colored("[FAIL]", BOLD_RED), name);
|
||||
println!("{} {}", "[FAIL]".style(ERROR_ST), name);
|
||||
fail += 1;
|
||||
parsed_any = true;
|
||||
continue;
|
||||
@@ -207,17 +220,17 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
|
||||
if let Some((status, name)) = line.split_once('\t') {
|
||||
match status {
|
||||
"PASS" => {
|
||||
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
|
||||
println!("{} {}", "[PASS]".style(SUCCESS_ST), name);
|
||||
pass += 1;
|
||||
parsed_any = true;
|
||||
}
|
||||
"SKIP" => {
|
||||
println!("{} {}", colored("[SKIP]", BOLD_BLUE), name);
|
||||
println!("{} {}", "[SKIP]".style(SKIP_ST), name);
|
||||
skip += 1;
|
||||
parsed_any = true;
|
||||
}
|
||||
"FAIL" => {
|
||||
println!("{} {}", colored("[FAIL]", BOLD_RED), name);
|
||||
println!("{} {}", "[FAIL]".style(ERROR_ST), name);
|
||||
fail += 1;
|
||||
parsed_any = true;
|
||||
}
|
||||
@@ -229,11 +242,11 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
|
||||
if !parsed_any {
|
||||
if suite_success {
|
||||
for name in &declared {
|
||||
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
|
||||
println!("{} {}", "[PASS]".style(SUCCESS_ST), name);
|
||||
}
|
||||
pass = declared.len() as i32;
|
||||
} else {
|
||||
println!("{} testsuite", colored("[FAIL]", BOLD_RED));
|
||||
println!("{} testsuite", "[FAIL]".style(ERROR_ST));
|
||||
fail = 1;
|
||||
}
|
||||
}
|
||||
@@ -245,14 +258,15 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
|
||||
};
|
||||
|
||||
println!();
|
||||
println!("{}", colored("=====================", BOLD_GREEN));
|
||||
println!("{}", colored(" Testsuite summary ", BOLD_GREEN));
|
||||
println!("{}", colored("=====================", BOLD_GREEN));
|
||||
sprintln!(BOLD_GREEN, "=====================");
|
||||
sprintln!(BOLD_GREEN, " Testsuite summary ");
|
||||
sprintln!(BOLD_GREEN, "=====================");
|
||||
println!("TOTAL: {}", total);
|
||||
print_field("PASS", pass, BOLD_GREEN);
|
||||
print_field("SKIP", skip, BOLD_BLUE);
|
||||
print_field("FAIL", fail, BOLD_RED);
|
||||
println!("{}", colored("=====================", BOLD_GREEN));
|
||||
print_field("PASS", pass, SUCCESS_ST);
|
||||
print_field("SKIP", skip, SKIP_ST);
|
||||
print_field("FAIL", fail, ERROR_ST);
|
||||
sprintln!(BOLD_GREEN, "=====================");
|
||||
sprintln!(BOLD_GREEN, "=====================");
|
||||
|
||||
if fail > 0 || !suite_success {
|
||||
return Ok(1);
|
||||
@@ -260,6 +274,7 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
/// Collects all .c source files from the tests/ directory.
|
||||
fn collect_test_sources() -> Result<Vec<PathBuf>, String> {
|
||||
let tests_dir = Path::new("./tests");
|
||||
if !tests_dir.is_dir() {
|
||||
@@ -278,6 +293,7 @@ fn collect_test_sources() -> Result<Vec<PathBuf>, String> {
|
||||
Ok(sources)
|
||||
}
|
||||
|
||||
/// Returns a list of include directories for test compilation.
|
||||
fn test_include_dirs(config: &Config, profile: &str) -> Vec<String> {
|
||||
let mut dirs = vec![
|
||||
"./tests".to_string(),
|
||||
@@ -288,6 +304,7 @@ fn test_include_dirs(config: &Config, profile: &str) -> Vec<String> {
|
||||
dirs
|
||||
}
|
||||
|
||||
/// Compiles a single test source file to an object file.
|
||||
fn compile_test_source(
|
||||
compiler: &str,
|
||||
standard: &str,
|
||||
@@ -317,6 +334,7 @@ fn compile_test_source(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Links the test object file into an executable binary.
|
||||
fn link_test_binary(
|
||||
compiler: &str,
|
||||
ldflags: &[String],
|
||||
@@ -343,6 +361,7 @@ fn link_test_binary(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prints the stdout and stderr from a command execution.
|
||||
fn print_command_output(output: &std::process::Output) {
|
||||
if !output.stdout.is_empty() {
|
||||
print!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
@@ -352,6 +371,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> {
|
||||
fs::create_dir_all("./tests").map_err(|_| "Failed to create tests/".to_string())?;
|
||||
fs::write("./tests/dcr_test.h", FILE_DCR_TEST_H)
|
||||
@@ -363,6 +383,7 @@ fn ensure_test_header() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extracts test case names from a C source file by parsing TEST_CASE macros.
|
||||
fn extract_test_names_path(path: &Path) -> Vec<String> {
|
||||
let Ok(content) = fs::read_to_string(path) else {
|
||||
return Vec::new();
|
||||
@@ -379,10 +400,11 @@ fn extract_test_names_path(path: &Path) -> Vec<String> {
|
||||
names
|
||||
}
|
||||
|
||||
fn print_field(label: &str, value: i32, color: &str) {
|
||||
/// Prints a test result field with color coding.
|
||||
fn print_field(label: &str, value: i32, style: Style) {
|
||||
if value == 0 {
|
||||
println!("{}: {}", label, value);
|
||||
} else {
|
||||
println!("{}{}: {}{}", color, label, value, RESET);
|
||||
sprintln!(style, "{}: {}", label, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,19 +15,25 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::core::build_config::Config;
|
||||
use crate::utils::log::error;
|
||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
|
||||
use crate::utils::cli_styles::{BOLD_CYAN, HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST};
|
||||
use crate::utils::log::sprintln;
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use toml::Value;
|
||||
|
||||
/// Displays the dependency tree of the current project.
|
||||
///
|
||||
/// This function checks for the --help flag and prints usage information if present.
|
||||
/// Otherwise, it parses the dcr.toml configuration and prints the dependency tree.
|
||||
pub fn tree(args: &[String]) -> i32 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
printc("USAGE:", BOLD_GREEN);
|
||||
printc(" dcr tree", BOLD_CYAN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
|
||||
sprintln!(HELP_EXAMPLES_ST, " dcr tree");
|
||||
println!();
|
||||
printc("DESCRIPTION:", BOLD_GREEN);
|
||||
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
|
||||
println!(" Displays the dependency tree of the current project.");
|
||||
return 0;
|
||||
}
|
||||
@@ -36,7 +42,7 @@ pub fn tree(args: &[String]) -> i32 {
|
||||
let config = match Config::open("dcr.toml") {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
error("dcr.toml not found in current directory");
|
||||
error!("dcr.toml not found in current directory");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
@@ -50,7 +56,7 @@ pub fn tree(args: &[String]) -> i32 {
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("0.0.0");
|
||||
|
||||
printc(&format!("{} v{}", name, version), BOLD_CYAN);
|
||||
sprintln!(BOLD_CYAN, "{} v{}", name, version);
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
seen.insert(name.to_string());
|
||||
@@ -62,6 +68,9 @@ pub fn tree(args: &[String]) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
/// Recursively prints the dependency tree starting from the given dependencies table.
|
||||
///
|
||||
/// Sorts dependencies for consistent ordering and handles path resolution for sub-dependencies.
|
||||
fn print_deps(
|
||||
deps: &toml::value::Table,
|
||||
base_path: &Path,
|
||||
@@ -81,6 +90,7 @@ fn print_deps(
|
||||
let mut sub_deps = None;
|
||||
let mut resolved_path = None;
|
||||
|
||||
// Resolve dependency path if it's a path-based dependency
|
||||
if let Some(path) = &dep_path {
|
||||
let dcr_toml = path.join("dcr.toml");
|
||||
if dcr_toml.exists()
|
||||
@@ -107,6 +117,19 @@ fn print_deps(
|
||||
format!(" v{}", v)
|
||||
} else if let Some(git) = t.get("git").and_then(|v| v.as_str()) {
|
||||
format!(" ({})", git)
|
||||
} else if let Some(pkg) = t.get("pkg-config").or_else(|| t.get("pkg_config")) {
|
||||
if let Some(s) = pkg.as_str() {
|
||||
format!(" (pkg-config: {})", s)
|
||||
} else if let Some(arr) = pkg.as_array() {
|
||||
let names = arr
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
format!(" (pkg-config: {})", names)
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
@@ -117,6 +140,7 @@ fn print_deps(
|
||||
|
||||
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
|
||||
&& !seen.contains(*name)
|
||||
{
|
||||
@@ -130,6 +154,7 @@ fn print_deps(
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the filesystem path for a dependency entry if it specifies a "path:" prefix or table with "path" key.
|
||||
fn resolve_dep_path(value: &Value, base_path: &Path) -> Option<PathBuf> {
|
||||
match value {
|
||||
Value::String(s) => s
|
||||
|
||||
@@ -96,6 +96,7 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
"#;
|
||||
|
||||
/// Returns build flags for the given profile as a static slice, or None if empty.
|
||||
pub fn flags(profile: &str) -> Option<&'static [&'static str]> {
|
||||
let f = crate::utils::build::default_profile_flags(profile);
|
||||
if f.is_empty() { None } else { Some(f) }
|
||||
|
||||
@@ -22,6 +22,7 @@ use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{self, Read, Seek, SeekFrom, Write};
|
||||
use std::path::Path;
|
||||
|
||||
/// File wrapper that shifts all seek offsets (used to skip the boot sector on a FAT image).
|
||||
struct OffsetFile<T> {
|
||||
inner: T,
|
||||
offset: u64,
|
||||
@@ -73,6 +74,7 @@ impl<T: Seek> Seek for OffsetFile<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a size string like `1.5m` or `500k` into bytes (1024-based; empty → 0).
|
||||
fn parse_size(s: &str) -> Result<u64, String> {
|
||||
let s = s.trim();
|
||||
if s.is_empty() {
|
||||
@@ -101,6 +103,7 @@ fn parse_size(s: &str) -> Result<u64, String> {
|
||||
Ok((val * multiplier) as u64)
|
||||
}
|
||||
|
||||
/// Creates a FAT image from layout config, substituting `{profile}` and optional boot sector.
|
||||
pub fn pack_archive(
|
||||
project_root: &Path,
|
||||
config: &ArchiveConfig,
|
||||
@@ -142,6 +145,7 @@ pub fn pack_archive(
|
||||
if offset == 0
|
||||
&& let Some(boot_rel) = &config.bootsector
|
||||
{
|
||||
// Load and write bootsector if present
|
||||
let boot_rel_substituted = boot_rel.replace("{profile}", profile);
|
||||
let boot_path = project_root.join(&boot_rel_substituted);
|
||||
if boot_path.is_file() {
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::common;
|
||||
use crate::core::build::{builder::BuildContext, common};
|
||||
use crate::platform;
|
||||
use crate::utils::build::is_bare_metal_target;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::time::Instant;
|
||||
|
||||
/// Returns the artifact directory path based on the build context.
|
||||
pub(crate) fn artifact_dir(ctx: &BuildContext) -> PathBuf {
|
||||
match ctx.target_dir {
|
||||
Some(dir) => Path::new(dir).to_path_buf(),
|
||||
@@ -30,6 +30,77 @@ pub(crate) fn artifact_dir(ctx: &BuildContext) -> PathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the artifact path for a given kind/profile/name.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `kind`: Artifact kind (`bin`, `staticlib`, `sharedlib`, `flat-bin`, `efi`, `elf`, …).
|
||||
/// - `profile`: Build profile (`debug` / `release` / custom).
|
||||
/// - `project_name`: Default basename when `output_filename` is unset.
|
||||
/// - `target_dir`: Optional artifact directory override.
|
||||
/// - `output_filename` / `output_extension`: Optional name/ext overrides.
|
||||
///
|
||||
/// # Returns
|
||||
/// Relative/absolute path string, or `None` for `none` / `custom` kinds.
|
||||
pub fn resolve_artifact_path(
|
||||
kind: &str,
|
||||
profile: &str,
|
||||
project_name: &str,
|
||||
target_dir: Option<&str>,
|
||||
output_filename: Option<&str>,
|
||||
output_extension: Option<&str>,
|
||||
) -> Option<String> {
|
||||
if crate::utils::build::is_flat_bin(kind) {
|
||||
let name = output_filename.unwrap_or(project_name);
|
||||
let ext = output_extension.unwrap_or("bin");
|
||||
let file = if ext.is_empty() {
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("{name}.{ext}")
|
||||
};
|
||||
let dir = match target_dir {
|
||||
Some(dir) => Path::new(dir).to_path_buf(),
|
||||
None => Path::new("./target").join(profile),
|
||||
};
|
||||
return Some(dir.join(file).to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
if matches!(kind, "none" | "custom") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let name = output_filename.unwrap_or(project_name);
|
||||
let ext = output_extension.unwrap_or("");
|
||||
let final_name = if ext.is_empty() {
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("{}.{}", name, ext)
|
||||
};
|
||||
|
||||
let path = match kind {
|
||||
"staticlib" => platform::lib_path(profile, &final_name, target_dir),
|
||||
"sharedlib" => platform::shared_lib_path(profile, &final_name, target_dir),
|
||||
"efi" => platform::efi_path(profile, &final_name, target_dir),
|
||||
"elf" => platform::elf_path(profile, &final_name, target_dir),
|
||||
_ => platform::bin_path(profile, &final_name, target_dir),
|
||||
};
|
||||
Some(path)
|
||||
}
|
||||
|
||||
/// Converts a relative or absolute path to an absolute path by joining with project root if needed.
|
||||
pub fn absolute_artifact_path(project_root: &Path, relative_or_abs: &str) -> PathBuf {
|
||||
let p = Path::new(relative_or_abs);
|
||||
let joined = if p.is_absolute() {
|
||||
p.to_path_buf()
|
||||
} else {
|
||||
let stripped = relative_or_abs
|
||||
.strip_prefix("./")
|
||||
.unwrap_or(relative_or_abs);
|
||||
project_root.join(stripped)
|
||||
};
|
||||
crate::utils::fs::canonicalize_path(&joined)
|
||||
}
|
||||
|
||||
/// Returns the output path for a flat binary artifact.
|
||||
pub(crate) fn flat_output_path(ctx: &BuildContext) -> String {
|
||||
let name = ctx.output_filename.unwrap_or(ctx.project_name);
|
||||
let ext = ctx.output_extension.unwrap_or("bin");
|
||||
@@ -41,6 +112,7 @@ pub(crate) fn flat_output_path(ctx: &BuildContext) -> String {
|
||||
artifact_dir(ctx).join(file).to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
/// Returns the output path for a flat binary artifact from a source file.
|
||||
pub(crate) fn flat_source_output_path(ctx: &BuildContext, source: &str) -> String {
|
||||
let stem = Path::new(source)
|
||||
.file_stem()
|
||||
@@ -55,6 +127,7 @@ pub(crate) fn flat_source_output_path(ctx: &BuildContext, source: &str) -> Strin
|
||||
artifact_dir(ctx).join(file).to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
// Helper to find a suitable objcopy tool in PATH
|
||||
fn resolve_objcopy() -> Result<String, String> {
|
||||
for candidate in ["llvm-objcopy", "objcopy", "gobjcopy"] {
|
||||
if Command::new(candidate)
|
||||
@@ -73,6 +146,7 @@ fn resolve_objcopy() -> Result<String, String> {
|
||||
)
|
||||
}
|
||||
|
||||
/// Converts an object/ELF to a raw binary via `objcopy -O binary` (skips if up to date).
|
||||
pub(crate) fn objcopy_binary(ctx: &BuildContext, input: &str, output: &str) -> Result<(), String> {
|
||||
if let Some(parent) = Path::new(output).parent() {
|
||||
std::fs::create_dir_all(parent).map_err(|e| format!("output dir error: {e}"))?;
|
||||
@@ -89,6 +163,7 @@ pub(crate) fn objcopy_binary(ctx: &BuildContext, input: &str, output: &str) -> R
|
||||
common::run_command_sync_output(&mut cmd)
|
||||
}
|
||||
|
||||
/// Links objects into an intermediate ELF, then converts it to a flat binary via objcopy.
|
||||
pub(crate) fn link_flat_binary(
|
||||
ctx: &BuildContext,
|
||||
objects: &[String],
|
||||
@@ -150,6 +225,7 @@ pub(crate) fn link_flat_binary(
|
||||
Ok(common::elapsed_secs(start_time))
|
||||
}
|
||||
|
||||
/// Archives objects into a static library.
|
||||
pub(crate) fn archive_static(
|
||||
ctx: &BuildContext,
|
||||
objects: &[String],
|
||||
@@ -176,6 +252,7 @@ pub(crate) fn archive_static(
|
||||
run(ctx, cmd, start_time)
|
||||
}
|
||||
|
||||
/// Links objects into a binary or library based on artifact kind.
|
||||
pub(crate) fn link_binary(
|
||||
ctx: &BuildContext,
|
||||
objects: &[String],
|
||||
@@ -241,6 +318,7 @@ pub(crate) fn link_binary(
|
||||
run(ctx, cmd, start_time)
|
||||
}
|
||||
|
||||
/// Returns the default archiver name based on compiler and platform.
|
||||
fn default_archiver(compiler: &str) -> &'static str {
|
||||
let c = compiler.to_lowercase();
|
||||
if cfg!(target_os = "windows") {
|
||||
@@ -256,11 +334,13 @@ fn default_archiver(compiler: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
@@ -282,3 +362,64 @@ fn run(ctx: &BuildContext, mut cmd: Command, start_time: Instant) -> Result<f64,
|
||||
Err(format!("Build failed:\nstdout: {stdout}\nstderr: {stderr}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Default `bin` kind resolves via platform bin path.
|
||||
#[test]
|
||||
fn resolve_bin_default() {
|
||||
let p = resolve_artifact_path("bin", "debug", "hello", None, None, None).unwrap();
|
||||
assert_eq!(p, platform::bin_path("debug", "hello", None));
|
||||
}
|
||||
|
||||
/// Custom `target_dir` places the binary under that directory.
|
||||
#[test]
|
||||
fn resolve_bin_custom_target_dir() {
|
||||
let p = resolve_artifact_path("bin", "debug", "hello", Some("/out"), None, None).unwrap();
|
||||
let name = if cfg!(windows) { "hello.exe" } else { "hello" };
|
||||
let expected = Path::new("/out").join(name);
|
||||
assert_eq!(p, expected.to_string_lossy());
|
||||
}
|
||||
|
||||
/// `staticlib` kind resolves to a platform static library path.
|
||||
#[test]
|
||||
fn resolve_staticlib() {
|
||||
let p = resolve_artifact_path("staticlib", "release", "mylib", Some("dist"), None, None)
|
||||
.unwrap();
|
||||
let name = if cfg!(windows) {
|
||||
"mylib.lib"
|
||||
} else {
|
||||
"libmylib.a"
|
||||
};
|
||||
let expected = Path::new("dist").join(name);
|
||||
assert_eq!(p, expected.to_string_lossy());
|
||||
}
|
||||
|
||||
/// Custom output filename and extension override the default name.
|
||||
#[test]
|
||||
fn resolve_with_filename_ext() {
|
||||
let p = resolve_artifact_path(
|
||||
"bin",
|
||||
"debug",
|
||||
"pkg",
|
||||
Some("/out"),
|
||||
Some("KERNEL"),
|
||||
Some("ELF"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
p,
|
||||
std::path::Path::new("/out")
|
||||
.join("KERNEL.ELF")
|
||||
.to_string_lossy()
|
||||
);
|
||||
}
|
||||
|
||||
/// `none` kind has no artifact path.
|
||||
#[test]
|
||||
fn resolve_none_kind() {
|
||||
assert!(resolve_artifact_path("none", "debug", "x", None, None, None).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,21 +15,23 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::builder::artifact;
|
||||
use crate::core::build::common;
|
||||
use crate::core::build::{builder::BuildContext, builder::artifact, common};
|
||||
use crate::utils::build::{is_bare_metal_target, is_compile_only, is_flat_bin};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::time::Instant;
|
||||
|
||||
/// Compiles C/C++ sources and links/archives by kind (`bin`, libs, `flat-bin`, or compile-only).
|
||||
///
|
||||
/// Handles Qt preprocessing when enabled. Returns elapsed seconds.
|
||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
let compiler = if ctx.compiler.is_empty() {
|
||||
"cc"
|
||||
} else {
|
||||
ctx.compiler
|
||||
};
|
||||
// Reject unsupported flat-bin with Qt
|
||||
if is_flat_bin(ctx.kind) && ctx.qt {
|
||||
return Err("flat-bin is not supported with build.qt = true".to_string());
|
||||
}
|
||||
@@ -69,6 +71,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
artifact::link_binary(ctx, &objects, compiler, start_time)
|
||||
}
|
||||
|
||||
/// Collects source files from the provided source roots for the build context.
|
||||
///
|
||||
/// Delegates to common source collection after resolving language-specific extensions.
|
||||
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
let extensions = source_extensions(ctx.language);
|
||||
common::collect_sources(
|
||||
@@ -79,14 +84,19 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
|
||||
)
|
||||
}
|
||||
|
||||
/// Source file extensions for `language` (owned `Vec` of static slices).
|
||||
fn source_extensions(language: &str) -> Vec<&'static str> {
|
||||
crate::core::build::common::source_extensions(language)
|
||||
}
|
||||
|
||||
/// Retrieves default compiler flags for the specified build profile.
|
||||
fn default_flags(profile: &str) -> &'static [&'static str] {
|
||||
crate::utils::build::default_profile_flags(profile)
|
||||
}
|
||||
|
||||
/// Compiles all provided sources into object files in parallel.
|
||||
///
|
||||
/// Prepares object file paths and spawns parallel tasks for each source compilation.
|
||||
fn build_objects(
|
||||
compiler: &str,
|
||||
sources: &[String],
|
||||
@@ -100,6 +110,7 @@ fn build_objects(
|
||||
.map(|s| common::object_path(obj_dir, s, obj_ext))
|
||||
.collect();
|
||||
|
||||
// Compile objects in parallel
|
||||
common::parallel_build(
|
||||
sources.len(),
|
||||
|i| {
|
||||
@@ -117,6 +128,9 @@ fn build_objects(
|
||||
Ok(objects)
|
||||
}
|
||||
|
||||
/// Compiles a single source file to an object file.
|
||||
///
|
||||
/// Creates object directory if needed, checks for rebuild necessity, constructs and runs the compiler command with appropriate flags.
|
||||
fn build_object(
|
||||
compiler: &str,
|
||||
source: &str,
|
||||
@@ -128,6 +142,7 @@ fn build_object(
|
||||
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
|
||||
}
|
||||
|
||||
// Skip if object is already up to date
|
||||
if !common::needs_rebuild(source, obj_path) {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -183,6 +198,8 @@ fn build_object(
|
||||
|
||||
let use_dcr_defaults = ctx.cflags.is_empty()
|
||||
&& !(ctx.freestanding || is_bare_metal_target(ctx.target) || is_flat_bin(ctx.kind));
|
||||
|
||||
// Use default flags only if no custom flags and not special target
|
||||
if use_dcr_defaults {
|
||||
for flag in default_flags(ctx.profile) {
|
||||
cmd.arg(flag);
|
||||
@@ -211,6 +228,7 @@ fn build_object(
|
||||
common::run_command_sync_output(&mut cmd)
|
||||
}
|
||||
|
||||
/// Returns the assembly language flag for the compiler based on source extension.
|
||||
fn asm_lang_flag(source: &str) -> Option<&'static str> {
|
||||
crate::core::build::common::asm_lang_flag(source)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ pub mod msvc;
|
||||
use crate::core::build::language::asm::{fasm, gas, masm, nasm};
|
||||
use crate::core::build::language::llvm_ir;
|
||||
|
||||
/// Configuration context for the build process, including compiler, target, and output settings.
|
||||
pub struct BuildContext<'a> {
|
||||
pub profile: &'a str,
|
||||
pub project_name: &'a str,
|
||||
@@ -56,6 +57,7 @@ pub struct BuildContext<'a> {
|
||||
pub qt: bool,
|
||||
}
|
||||
|
||||
/// Trait defining the build interface for different compilers and languages.
|
||||
pub trait Builder {
|
||||
#[allow(dead_code)]
|
||||
fn id(&self) -> &'static str;
|
||||
@@ -63,12 +65,19 @@ pub trait Builder {
|
||||
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String>;
|
||||
}
|
||||
|
||||
/// Concrete builder for C/C++ languages using gcc/clang.
|
||||
struct Cc;
|
||||
/// Concrete builder for MSVC and clang-cl.
|
||||
struct Msvc;
|
||||
/// Concrete builder for NASM assembly.
|
||||
struct Nasm;
|
||||
/// Concrete builder for GNU Assembler (gas).
|
||||
struct Gas;
|
||||
/// Concrete builder for MASM assembler.
|
||||
struct Masm;
|
||||
/// Concrete builder for FASM assembler.
|
||||
struct Fasm;
|
||||
/// Concrete builder for LLVM IR.
|
||||
struct Llc;
|
||||
|
||||
impl Builder for Cc {
|
||||
@@ -156,6 +165,7 @@ impl Builder for Llc {
|
||||
}
|
||||
|
||||
pub fn builder_for(compiler: &str) -> &'static dyn Builder {
|
||||
// Normalize compiler name to lowercase for matching.
|
||||
let c = compiler.to_lowercase();
|
||||
if c.contains("clang-cl") {
|
||||
return &Msvc;
|
||||
@@ -181,6 +191,7 @@ pub fn builder_for(compiler: &str) -> &'static dyn Builder {
|
||||
&Cc
|
||||
}
|
||||
|
||||
/// Main build entrypoint that validates the compiler and delegates to the selected builder.
|
||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
if !check_compiler_exists(ctx.compiler) {
|
||||
return Err(format!(
|
||||
@@ -188,9 +199,11 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
ctx.compiler
|
||||
));
|
||||
}
|
||||
// Delegate to the concrete builder's build method.
|
||||
builder_for(ctx.compiler).build(ctx)
|
||||
}
|
||||
|
||||
/// True if `compiler --version` can be spawned (exit status is ignored).
|
||||
fn check_compiler_exists(compiler: &str) -> bool {
|
||||
let name = if compiler.is_empty() {
|
||||
"cc"
|
||||
@@ -209,10 +222,12 @@ fn check_compiler_exists(compiler: &str) -> bool {
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Collects source code files by delegating to the builder selected for the compiler.
|
||||
pub fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
builder_for(ctx.compiler).collect_sources(ctx)
|
||||
}
|
||||
|
||||
/// Unit test verifying that builder_for correctly identifies the builder for various compiler names.
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -15,16 +15,17 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::builder::artifact;
|
||||
use crate::core::build::common;
|
||||
/// MSVC build backend for DCR.
|
||||
///
|
||||
/// This module provides the MSVC-specific build implementation for C and C++
|
||||
/// projects, including object compilation and linking for executables, static
|
||||
/// libraries, shared libraries, and flat binaries.
|
||||
use crate::core::build::{builder::BuildContext, builder::artifact, common};
|
||||
use crate::platform;
|
||||
use crate::utils::build::{is_compile_only, is_flat_bin};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::time::Instant;
|
||||
use std::{fs, path::Path, process::Command, time::Instant};
|
||||
|
||||
/// Builds the project target using the MSVC compiler.
|
||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
let compiler = if ctx.compiler.is_empty() {
|
||||
"cl"
|
||||
@@ -159,6 +160,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Links the flat binary output for MSVC build targets.
|
||||
fn link_flat_msvc(
|
||||
ctx: &BuildContext,
|
||||
compiler: &str,
|
||||
@@ -214,6 +216,7 @@ fn link_flat_msvc(
|
||||
Ok(common::elapsed_secs(start_time))
|
||||
}
|
||||
|
||||
/// Collects all source files for the build based on language extensions.
|
||||
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
let extensions = source_extensions(ctx.language);
|
||||
common::collect_sources(
|
||||
@@ -224,10 +227,12 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
|
||||
)
|
||||
}
|
||||
|
||||
/// Source file extensions for `language` (owned `Vec` of static slices).
|
||||
fn source_extensions(language: &str) -> Vec<&'static str> {
|
||||
crate::core::build::common::source_extensions(language)
|
||||
}
|
||||
|
||||
/// Maps the language and standard version to the corresponding MSVC flag string.
|
||||
fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> {
|
||||
let lang = language.to_lowercase();
|
||||
let std = standard.to_lowercase();
|
||||
@@ -251,6 +256,7 @@ fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String>
|
||||
Err("Unsupported language".to_string())
|
||||
}
|
||||
|
||||
/// Returns the MSVC architecture flag based on the target platform.
|
||||
fn msvc_arch_flag(platform: Option<&str>) -> Option<&'static str> {
|
||||
let raw = platform?.trim();
|
||||
if raw.is_empty() {
|
||||
@@ -268,6 +274,7 @@ fn msvc_arch_flag(platform: Option<&str>) -> Option<&'static str> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns default compiler flags for the specified build profile.
|
||||
fn default_flags(profile: &str) -> &'static [&'static str] {
|
||||
match profile {
|
||||
"release" => &["/O2", "/DNDEBUG"],
|
||||
@@ -276,6 +283,7 @@ fn default_flags(profile: &str) -> &'static [&'static str] {
|
||||
}
|
||||
}
|
||||
|
||||
/// Compiles all sources into object files using parallel processing.
|
||||
fn build_objects(
|
||||
compiler: &str,
|
||||
sources: &[String],
|
||||
@@ -297,6 +305,7 @@ fn build_objects(
|
||||
Ok(objects)
|
||||
}
|
||||
|
||||
/// Compiles a single source file to an object file.
|
||||
fn build_object(
|
||||
compiler: &str,
|
||||
source: &str,
|
||||
@@ -369,6 +378,7 @@ fn build_object(
|
||||
let mut headers = Vec::new();
|
||||
let mut clean_stdout = String::new();
|
||||
|
||||
// Parse compiler output to extract include file notes for dependency generation
|
||||
for line in stdout.lines() {
|
||||
if let Some(stripped) = line.strip_prefix("Note: including file:") {
|
||||
headers.push(stripped.trim().to_string());
|
||||
|
||||
@@ -15,12 +15,22 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::common;
|
||||
use crate::core::build::{builder::BuildContext, common};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Computes a build fingerprint by hashing context fields and file metadata (SHA-256).
|
||||
///
|
||||
/// Includes `dcr.toml` (required) and `dcr.lock` when present, plus path/size/mtime
|
||||
/// for each source, header, and library file.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `ctx`: Build context (profile, tools, flags, dirs, …).
|
||||
/// - `sources` / `headers` / `lib_files`: Inputs whose metadata enters the hash.
|
||||
///
|
||||
/// # Returns
|
||||
/// Hex-encoded fingerprint, or an error if `dcr.toml` or a listed file cannot be read.
|
||||
pub(crate) fn compute_build_fingerprint(
|
||||
ctx: &BuildContext,
|
||||
sources: &[String],
|
||||
@@ -61,12 +71,14 @@ pub(crate) fn compute_build_fingerprint(
|
||||
for value in ctx.ldflags {
|
||||
hasher.update(value.as_bytes());
|
||||
}
|
||||
// Include project metadata from dcr.toml in the fingerprint
|
||||
let toml =
|
||||
fs::read_to_string("dcr.toml").map_err(|err| format!("Failed to read dcr.toml: {err}"))?;
|
||||
hasher.update(toml.as_bytes());
|
||||
if let Ok(lock) = fs::read_to_string("dcr.lock") {
|
||||
hasher.update(lock.as_bytes());
|
||||
}
|
||||
// Include source, header, and library files in the fingerprint
|
||||
for source in sources {
|
||||
let path = Path::new(source);
|
||||
update_hasher_with_file(&mut hasher, path)?;
|
||||
@@ -80,6 +92,14 @@ pub(crate) fn compute_build_fingerprint(
|
||||
Ok(to_hex(&hasher.finalize()))
|
||||
}
|
||||
|
||||
/// Whether the build can be skipped: main artifact exists and cache hash matches.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `ctx`: Used to locate the output artifact and `.dcr-build.hash`.
|
||||
/// - `fingerprint`: Fresh hash from [`compute_build_fingerprint`].
|
||||
///
|
||||
/// # Returns
|
||||
/// `true` only when both the artifact and matching cache file are present.
|
||||
pub(crate) fn should_skip_build(ctx: &BuildContext, fingerprint: &str) -> bool {
|
||||
let output = build_output_path(ctx);
|
||||
if !Path::new(&output).is_file() {
|
||||
@@ -90,6 +110,7 @@ pub(crate) fn should_skip_build(ctx: &BuildContext, fingerprint: &str) -> bool {
|
||||
cached.trim() == fingerprint
|
||||
}
|
||||
|
||||
/// Persists the build fingerprint to the cache file on disk.
|
||||
pub(crate) fn write_build_fingerprint(ctx: &BuildContext, fingerprint: &str) -> Result<(), String> {
|
||||
let cache_path = build_cache_path(ctx.profile, ctx.target_dir);
|
||||
if let Some(parent) = cache_path.parent() {
|
||||
@@ -99,6 +120,7 @@ pub(crate) fn write_build_fingerprint(ctx: &BuildContext, fingerprint: &str) ->
|
||||
.map_err(|err| format!("Failed to write cache: {err}"))
|
||||
}
|
||||
|
||||
/// Returns the path to the build cache hash file based on profile and target directory.
|
||||
fn build_cache_path(profile: &str, target_dir: Option<&str>) -> PathBuf {
|
||||
match target_dir {
|
||||
Some(dir) => Path::new(dir).join(".dcr-build.hash"),
|
||||
@@ -106,35 +128,26 @@ fn build_cache_path(profile: &str, target_dir: Option<&str>) -> PathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_output_path(ctx: &BuildContext) -> String {
|
||||
if crate::utils::build::is_flat_bin(ctx.kind) {
|
||||
return crate::core::build::builder::artifact::flat_output_path(ctx);
|
||||
}
|
||||
|
||||
let name = ctx.output_filename.unwrap_or(ctx.project_name);
|
||||
let ext = ctx.output_extension.unwrap_or("");
|
||||
|
||||
let final_name = if ext.is_empty() {
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("{}.{}", name, ext)
|
||||
};
|
||||
|
||||
if ctx.kind == "staticlib" {
|
||||
return crate::platform::lib_path(ctx.profile, &final_name, ctx.target_dir);
|
||||
}
|
||||
if ctx.kind == "sharedlib" {
|
||||
return crate::platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir);
|
||||
}
|
||||
if ctx.kind == "efi" {
|
||||
return crate::platform::efi_path(ctx.profile, &final_name, ctx.target_dir);
|
||||
}
|
||||
if ctx.kind == "elf" {
|
||||
return crate::platform::elf_path(ctx.profile, &final_name, ctx.target_dir);
|
||||
}
|
||||
crate::platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
|
||||
/// Resolves the output path for the build artifact using the provided context.
|
||||
pub fn build_output_path(ctx: &BuildContext) -> String {
|
||||
crate::core::build::builder::artifact::resolve_artifact_path(
|
||||
ctx.kind,
|
||||
ctx.profile,
|
||||
ctx.project_name,
|
||||
ctx.target_dir,
|
||||
ctx.output_filename,
|
||||
ctx.output_extension,
|
||||
)
|
||||
.unwrap_or_else(|| {
|
||||
crate::platform::bin_path(
|
||||
ctx.profile,
|
||||
ctx.output_filename.unwrap_or(ctx.project_name),
|
||||
ctx.target_dir,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Collects header files from source roots and include directories, filtering out excluded paths.
|
||||
pub(crate) fn collect_header_files(
|
||||
ctx: &BuildContext,
|
||||
project_root: &Path,
|
||||
@@ -176,6 +189,7 @@ pub(crate) fn collect_header_files(
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Recursively scans a directory to collect header files, respecting exclusion rules.
|
||||
fn collect_header_files_rec(
|
||||
dir: &Path,
|
||||
out: &mut Vec<PathBuf>,
|
||||
@@ -208,11 +222,13 @@ fn collect_header_files_rec(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if the given path has a header file extension.
|
||||
fn is_header_file(path: &Path) -> bool {
|
||||
let ext = path.extension().and_then(|v| v.to_str()).unwrap_or("");
|
||||
matches!(ext, "h" | "hpp" | "hh" | "hxx" | "inc")
|
||||
}
|
||||
|
||||
/// Collects library files from lib directories using platform-specific candidates.
|
||||
pub(crate) fn collect_lib_files(ctx: &BuildContext) -> Vec<PathBuf> {
|
||||
let mut out = Vec::new();
|
||||
for dir in ctx.lib_dirs {
|
||||
@@ -229,6 +245,7 @@ pub(crate) fn collect_lib_files(ctx: &BuildContext) -> Vec<PathBuf> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Candidate library filenames for the **host** OS (`cfg!(target_os = …)`).
|
||||
fn lib_candidates(name: &str) -> Vec<String> {
|
||||
if cfg!(target_os = "windows") {
|
||||
return vec![format!("{name}.lib")];
|
||||
@@ -247,6 +264,7 @@ fn lib_candidates(name: &str) -> Vec<String> {
|
||||
]
|
||||
}
|
||||
|
||||
/// Updates the hasher with the file's path, size, and last modification time.
|
||||
fn update_hasher_with_file(hasher: &mut Sha256, path: &Path) -> Result<(), String> {
|
||||
hasher.update(path.to_string_lossy().as_bytes());
|
||||
let meta = fs::metadata(path).map_err(|err| format!("source read error: {err}"))?;
|
||||
@@ -259,6 +277,7 @@ fn update_hasher_with_file(hasher: &mut Sha256, path: &Path) -> Result<(), Strin
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Converts a byte slice containing the SHA256 hash to a hexadecimal string.
|
||||
fn to_hex(bytes: &[u8]) -> String {
|
||||
crate::utils::fs::to_hex(bytes)
|
||||
}
|
||||
|
||||
@@ -27,18 +27,25 @@ static OUTPUT_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
static PROGRESS_LABEL: OnceLock<Mutex<Option<String>>> = OnceLock::new();
|
||||
static PROGRESS_DIRTY: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Returns a static mutex used to synchronize access to stderr during progress reporting.
|
||||
/// This ensures thread-safe output when multiple build threads write to the terminal.
|
||||
pub fn get_output_lock() -> &'static Mutex<()> {
|
||||
OUTPUT_MUTEX.get_or_init(|| Mutex::new(()))
|
||||
}
|
||||
|
||||
/// Initializes the shared progress label storage.
|
||||
fn progress_label_lock() -> &'static Mutex<Option<String>> {
|
||||
PROGRESS_LABEL.get_or_init(|| Mutex::new(None))
|
||||
}
|
||||
|
||||
/// Sets the label text that will be shown next to the compile progress counter.
|
||||
/// The label is cleared after the progress line is finished.
|
||||
pub fn set_progress_label(label: Option<String>) {
|
||||
*progress_label_lock().lock().unwrap() = label;
|
||||
}
|
||||
|
||||
/// Finishes the current progress line by clearing the label and flushing the line if it was dirty.
|
||||
/// This is called when a build step completes normally.
|
||||
pub fn finish_progress_line() {
|
||||
let was_dirty = PROGRESS_DIRTY.swap(false, Ordering::SeqCst);
|
||||
*progress_label_lock().lock().unwrap() = None;
|
||||
@@ -52,6 +59,8 @@ pub fn finish_progress_line() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Interrupts the current progress line (if any) and flushes it to stderr.
|
||||
/// Used when a build step produces output that should interrupt the progress display.
|
||||
pub fn interrupt_progress_for_output() {
|
||||
if !PROGRESS_DIRTY.swap(false, Ordering::SeqCst) {
|
||||
return;
|
||||
@@ -63,6 +72,9 @@ pub fn interrupt_progress_for_output() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reports the current compile progress to stderr in the format "label [done/total]".
|
||||
/// Only prints if the terminal is attached and the label is set.
|
||||
/// Updates the dirty flag so the line can be overwritten on the next call.
|
||||
fn report_compile_progress(done: usize, total: usize) {
|
||||
let label = progress_label_lock().lock().unwrap().clone();
|
||||
let Some(label) = label else {
|
||||
@@ -82,6 +94,9 @@ fn report_compile_progress(done: usize, total: usize) {
|
||||
PROGRESS_DIRTY.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Recursively collects all source files under the given roots that match the provided extensions.
|
||||
/// Directories listed in exclude_dirs are skipped unless overridden by include_paths.
|
||||
/// Returns a sorted list of normalized source paths or an error string.
|
||||
pub fn collect_sources(
|
||||
roots: &[PathBuf],
|
||||
extensions: &[&str],
|
||||
@@ -104,6 +119,8 @@ pub fn collect_sources(
|
||||
Ok(sources)
|
||||
}
|
||||
|
||||
/// Recursively walks a directory tree to collect source files.
|
||||
/// Skips excluded directories unless they are explicitly allowed by include_paths.
|
||||
fn collect_sources_rec(
|
||||
dir: &Path,
|
||||
extensions: &[&str],
|
||||
@@ -151,6 +168,8 @@ fn collect_sources_rec(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Determines whether a path should be excluded from source collection.
|
||||
/// Returns false if the path is explicitly allowed by include_paths (even if it matches an exclude).
|
||||
pub fn is_excluded(path: &Path, exclude_dirs: &[PathBuf], include_paths: &[String]) -> bool {
|
||||
if matches_patterns(path, include_paths, true) {
|
||||
return false;
|
||||
@@ -161,6 +180,8 @@ pub fn is_excluded(path: &Path, exclude_dirs: &[PathBuf], include_paths: &[Strin
|
||||
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;
|
||||
@@ -180,6 +201,9 @@ fn path_is_under(path: &Path, prefix: &Path) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches a path against include/exclude glob and path patterns.
|
||||
/// Positive patterns allow files; negative patterns (starting with !) exclude them.
|
||||
/// Returns true if the path matches any rule.
|
||||
fn matches_patterns(path: &Path, patterns: &[String], positive: bool) -> bool {
|
||||
if patterns.is_empty() {
|
||||
return false;
|
||||
@@ -244,10 +268,20 @@ fn matches_patterns(path: &Path, patterns: &[String], positive: bool) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns true if the string contains any glob magic characters (*, ?, or [).
|
||||
pub fn has_glob_magic(value: &str) -> bool {
|
||||
value.chars().any(|c| matches!(c, '*' | '?' | '['))
|
||||
}
|
||||
|
||||
/// Returns a GCC/Clang `-x` language for assembly sources.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `source`: Source file path (extension decides the dialect).
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Some("assembler-with-cpp")` for `.S`
|
||||
/// - `Some("assembler")` for `.s` / `.asm`
|
||||
/// - `None` for other extensions
|
||||
pub fn asm_lang_flag(source: &str) -> Option<&'static str> {
|
||||
let ext = Path::new(source).extension().and_then(|v| v.to_str())?;
|
||||
match ext {
|
||||
@@ -257,6 +291,8 @@ pub fn asm_lang_flag(source: &str) -> Option<&'static str> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the list of file extensions associated with the given language token(s).
|
||||
/// Splits comma-separated tokens and falls back to .c if no language is recognized.
|
||||
pub fn source_extensions(language: &str) -> Vec<&'static str> {
|
||||
let mut out = Vec::new();
|
||||
for part in language.split(',').map(|s| s.trim()) {
|
||||
@@ -270,10 +306,13 @@ pub fn source_extensions(language: &str) -> Vec<&'static str> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Returns elapsed time in seconds with two decimal places for display purposes.
|
||||
pub fn elapsed_secs(start: Instant) -> f64 {
|
||||
((start.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0
|
||||
}
|
||||
|
||||
/// Spawns a fixed number of worker threads to parallelize a build task.
|
||||
/// Reports progress after each task completes.
|
||||
pub fn parallel_build<F>(total_tasks: usize, task_fn: F, max_jobs: usize) -> Result<(), String>
|
||||
where
|
||||
F: Fn(usize) -> Result<(), String> + Sync + Send,
|
||||
@@ -330,6 +369,8 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs a command, prints stdout/stderr to the terminal (interrupting progress),
|
||||
/// and returns `Ok(())` or `Err` based on exit status.
|
||||
pub fn run_command_sync_output(cmd: &mut Command) -> Result<(), String> {
|
||||
let program = cmd.get_program().to_string_lossy().into_owned();
|
||||
let output = cmd.output().map_err(|err| {
|
||||
@@ -357,6 +398,8 @@ pub fn run_command_sync_output(cmd: &mut Command) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalizes a source path to be relative to the current working directory when possible.
|
||||
/// Returns the original path unchanged if it cannot be made relative.
|
||||
pub fn normalize_source_path(path: &Path) -> String {
|
||||
if !path.is_absolute() {
|
||||
return path.to_string_lossy().to_string();
|
||||
@@ -369,6 +412,8 @@ pub fn normalize_source_path(path: &Path) -> String {
|
||||
path.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
/// Computes the output object file path by stripping the leading ./ and adjusting the extension.
|
||||
/// Places the object in the specified obj_dir while preserving directory structure.
|
||||
pub fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
|
||||
let src_path = Path::new(source);
|
||||
let stripped = src_path.strip_prefix("./").unwrap_or(src_path);
|
||||
@@ -401,6 +446,16 @@ pub fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
|
||||
out.to_string_lossy().replace('\\', "/")
|
||||
}
|
||||
|
||||
/// Checks whether a source file needs to be rebuilt.
|
||||
///
|
||||
/// Compares mtimes of the source, object, and deps listed in the sibling `.d` file.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `source`: Source path.
|
||||
/// - `object`: Object path (`.d` is derived by changing the extension).
|
||||
///
|
||||
/// # Returns
|
||||
/// `true` if the object is missing, older than source/deps, or a dep is missing.
|
||||
pub fn needs_rebuild(source: &str, object: &str) -> bool {
|
||||
let src_time = fs::metadata(source).and_then(|m| m.modified());
|
||||
let obj_time = fs::metadata(object).and_then(|m| m.modified());
|
||||
@@ -436,6 +491,14 @@ pub fn needs_rebuild(source: &str, object: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Checks whether the output executable needs to be relinked.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `objects`: Object file paths that feed the link step.
|
||||
/// - `output_path`: Linked artifact path.
|
||||
///
|
||||
/// # Returns
|
||||
/// `true` if the output is missing or any object is newer than it.
|
||||
pub fn needs_link(objects: &[String], output_path: &str) -> bool {
|
||||
let out_meta = match fs::metadata(output_path) {
|
||||
Ok(m) => m,
|
||||
@@ -461,6 +524,8 @@ pub fn needs_link(objects: &[String], output_path: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Parses a dependency file (.d) produced by GCC or MSVC compilers.
|
||||
/// Extracts all dependency paths, handling escaped newlines and Windows-style paths.
|
||||
fn parse_d_file(content: &str) -> Vec<String> {
|
||||
let mut deps = Vec::new();
|
||||
let mut target_end = 0;
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/// Core build engine module for DCR.
|
||||
/// This module provides the main build logic for projects and workspaces.
|
||||
use crate::core::build::builder::{BuildContext, build as build_project, collect_sources};
|
||||
use crate::core::build::cache::{
|
||||
collect_header_files, collect_lib_files, compute_build_fingerprint, should_skip_build,
|
||||
@@ -33,13 +35,14 @@ use crate::core::build::steps::{
|
||||
get_build_steps_with_profile, run_build_steps, verify_expectations,
|
||||
};
|
||||
use crate::core::build_config::Config;
|
||||
use crate::core::deps::{register, resolve_deps};
|
||||
use crate::core::deps::resolve_deps;
|
||||
use crate::core::workspace::parse_workspace;
|
||||
use crate::utils::build::{
|
||||
get_bool_with_profile, get_config_opt, get_config_str, get_language_with_profile,
|
||||
is_bare_metal_target, normalize_kind, normalize_platform, normalize_target,
|
||||
default_target_triple, get_bool_with_profile, get_config_opt, get_config_str,
|
||||
get_language_with_profile, is_bare_metal_target, normalize_kind, normalize_platform,
|
||||
normalize_target_os, parse_version_info, prepend_clang_target_flag, primary_language,
|
||||
resolve_compiler, resolve_pkg_config_flags, resolve_tool, substitute_vars,
|
||||
resolve_artifact_target_dir, resolve_compiler, resolve_pkg_config_flags, resolve_tool,
|
||||
substitute_vars,
|
||||
};
|
||||
use crate::utils::fs::check_dir;
|
||||
use std::fs;
|
||||
@@ -48,6 +51,7 @@ use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Instant;
|
||||
|
||||
/// Ensures that target directories exist for the build output.
|
||||
fn ensure_target_dirs(
|
||||
project_root: &Path,
|
||||
items: &[String],
|
||||
@@ -61,29 +65,13 @@ fn ensure_target_dirs(
|
||||
if let Some(dir) = &target_dir {
|
||||
let _ = fs::create_dir_all(dir);
|
||||
} else {
|
||||
let default_dir = if cfg!(target_os = "linux") {
|
||||
let arch = std::env::consts::ARCH;
|
||||
format!("{arch}-unknown-linux-gnu/{profile}")
|
||||
} else if cfg!(any(
|
||||
target_os = "freebsd",
|
||||
target_os = "openbsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "dragonfly"
|
||||
)) {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let os = std::env::consts::OS;
|
||||
format!("{arch}-unknown-{os}/{profile}")
|
||||
} else {
|
||||
profile.to_string()
|
||||
};
|
||||
let target_path = project_root.join("target");
|
||||
let target_items = check_dir(target_path.to_str()).unwrap_or_default();
|
||||
if !target_items.contains(&default_dir) {
|
||||
let _ = fs::create_dir_all(target_path.join(&default_dir));
|
||||
}
|
||||
let _ = fs::create_dir_all(
|
||||
project_root.join(crate::utils::build::native_host_target_rel(profile)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the project build and returns the elapsed time in seconds or an error.
|
||||
fn run_compile(
|
||||
ctx: &BuildContext,
|
||||
_cancel: &Arc<AtomicBool>,
|
||||
@@ -105,8 +93,16 @@ fn run_compile(
|
||||
}
|
||||
}
|
||||
|
||||
// Library entry point: run a build described by `req`, streaming progress to
|
||||
// `rep`. The CLI wraps this with a CliReporter; TUI/GUI/MCP plug in their own.
|
||||
/// Library entry point: run a build described by `req`, streaming progress to
|
||||
/// `rep`. The CLI wraps this with a CliReporter; TUI/GUI/MCP plug in their own.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `root`: Project or workspace root containing `dcr.toml`.
|
||||
/// - `req`: Profile, target, force/verbose, optional member, cancel flag.
|
||||
/// - `rep`: Progress sink for build events.
|
||||
///
|
||||
/// # Returns
|
||||
/// `BuildOutcome` with elapsed seconds, or `BuildError` on failure/cancel.
|
||||
pub fn run_build(
|
||||
root: &Path,
|
||||
req: &BuildRequest,
|
||||
@@ -126,6 +122,7 @@ pub fn run_build(
|
||||
Ok(BuildOutcome { secs })
|
||||
}
|
||||
|
||||
/// Orchestrates the build for the specified profile and target, handling workspaces and projects.
|
||||
#[allow(unused_variables, clippy::too_many_arguments)]
|
||||
fn build_all(
|
||||
root: &Path,
|
||||
@@ -159,6 +156,7 @@ fn build_all(
|
||||
&member.path,
|
||||
profile,
|
||||
target,
|
||||
target.is_some_and(|t| !t.trim().is_empty()),
|
||||
&[],
|
||||
force,
|
||||
verbose,
|
||||
@@ -186,22 +184,34 @@ fn build_all(
|
||||
let project_name = get_config_str(&config, "package.name");
|
||||
let project_version = get_config_str(&config, "package.version");
|
||||
|
||||
let targets_to_build: Vec<Option<String>> = if let Some(t) = target {
|
||||
vec![Some(normalize_target_os(t).to_string())]
|
||||
let targets_to_build: Vec<(Option<String>, bool)> = if let Some(t) = target {
|
||||
let normalized = normalize_target_os(t);
|
||||
if normalized.is_empty() {
|
||||
vec![(Some(default_target_triple()), true)]
|
||||
} else {
|
||||
vec![(Some(normalized.to_string()), true)]
|
||||
}
|
||||
} else {
|
||||
let config_targets = get_targets(&config, profile)?;
|
||||
if config_targets.is_empty() {
|
||||
vec![None]
|
||||
vec![(Some(default_target_triple()), false)]
|
||||
} else {
|
||||
config_targets
|
||||
.into_iter()
|
||||
.map(|t| Some(normalize_target_os(&t).to_string()))
|
||||
.map(|t| {
|
||||
let normalized = normalize_target_os(&t);
|
||||
if normalized.is_empty() {
|
||||
(Some(default_target_triple()), true)
|
||||
} else {
|
||||
(Some(normalized.to_string()), true)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
};
|
||||
|
||||
let start_time = Instant::now();
|
||||
for (i, build_target) in targets_to_build.iter().enumerate() {
|
||||
for (i, (build_target, explicit_target)) in targets_to_build.iter().enumerate() {
|
||||
if cancel.load(Ordering::SeqCst) {
|
||||
return Err("Build interrupted".to_string());
|
||||
}
|
||||
@@ -226,6 +236,7 @@ fn build_all(
|
||||
&member.path,
|
||||
profile,
|
||||
build_target.as_deref(),
|
||||
*explicit_target,
|
||||
&[],
|
||||
force,
|
||||
verbose,
|
||||
@@ -239,6 +250,7 @@ fn build_all(
|
||||
&ws,
|
||||
profile,
|
||||
build_target.as_deref(),
|
||||
*explicit_target,
|
||||
force,
|
||||
verbose,
|
||||
Some(root),
|
||||
@@ -251,6 +263,7 @@ fn build_all(
|
||||
root,
|
||||
profile,
|
||||
build_target.as_deref(),
|
||||
*explicit_target,
|
||||
&excludes,
|
||||
force,
|
||||
verbose,
|
||||
@@ -264,6 +277,7 @@ fn build_all(
|
||||
root,
|
||||
profile,
|
||||
build_target.as_deref(),
|
||||
*explicit_target,
|
||||
&[],
|
||||
force,
|
||||
verbose,
|
||||
@@ -278,11 +292,13 @@ fn build_all(
|
||||
Ok(elapsed)
|
||||
}
|
||||
|
||||
/// Builds each workspace member in order (flat loop, not recursive into nested workspaces).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build_workspace(
|
||||
workspace: &crate::core::workspace::Workspace,
|
||||
profile: &str,
|
||||
target: Option<&str>,
|
||||
explicit_target: bool,
|
||||
force: bool,
|
||||
verbose: bool,
|
||||
workspace_root: Option<&Path>,
|
||||
@@ -294,6 +310,7 @@ fn build_workspace(
|
||||
&member.path,
|
||||
profile,
|
||||
target,
|
||||
explicit_target,
|
||||
&[],
|
||||
force,
|
||||
verbose,
|
||||
@@ -305,17 +322,94 @@ fn build_workspace(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds a single project at the given root, handling configuration and steps.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build_project_at(
|
||||
project_root: &Path,
|
||||
profile: &str,
|
||||
target: Option<&str>,
|
||||
explicit_target: bool,
|
||||
exclude_dirs: &[std::path::PathBuf],
|
||||
force: bool,
|
||||
verbose: bool,
|
||||
workspace_root: Option<&Path>,
|
||||
cancel: &Arc<AtomicBool>,
|
||||
rep: &mut dyn BuildReporter,
|
||||
) -> Result<(), String> {
|
||||
let mut dependency_stack = Vec::new();
|
||||
build_project_at_with_stack(
|
||||
project_root,
|
||||
profile,
|
||||
target,
|
||||
explicit_target,
|
||||
exclude_dirs,
|
||||
force,
|
||||
verbose,
|
||||
workspace_root,
|
||||
cancel,
|
||||
rep,
|
||||
&mut dependency_stack,
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds a package while tracking the dependency chain to reject cycles.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build_project_at_with_stack(
|
||||
project_root: &Path,
|
||||
profile: &str,
|
||||
target: Option<&str>,
|
||||
explicit_target: bool,
|
||||
exclude_dirs: &[std::path::PathBuf],
|
||||
force: bool,
|
||||
verbose: bool,
|
||||
workspace_root: Option<&Path>,
|
||||
cancel: &Arc<AtomicBool>,
|
||||
rep: &mut dyn BuildReporter,
|
||||
dependency_stack: &mut Vec<PathBuf>,
|
||||
) -> Result<(), String> {
|
||||
let root = project_root
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| project_root.to_path_buf());
|
||||
if let Some(index) = dependency_stack.iter().position(|item| item == &root) {
|
||||
let mut chain: Vec<String> = dependency_stack[index..]
|
||||
.iter()
|
||||
.map(|item| item.display().to_string())
|
||||
.collect();
|
||||
chain.push(root.display().to_string());
|
||||
return Err(format!("Dependency cycle detected: {}", chain.join(" -> ")));
|
||||
}
|
||||
dependency_stack.push(root);
|
||||
let result = build_project_at_inner(
|
||||
project_root,
|
||||
profile,
|
||||
target,
|
||||
explicit_target,
|
||||
exclude_dirs,
|
||||
force,
|
||||
verbose,
|
||||
workspace_root,
|
||||
cancel,
|
||||
rep,
|
||||
dependency_stack,
|
||||
);
|
||||
dependency_stack.pop();
|
||||
result
|
||||
}
|
||||
|
||||
/// Builds one DCR package after the dependency-cycle guard has been installed.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build_project_at_inner(
|
||||
project_root: &Path,
|
||||
profile: &str,
|
||||
target: Option<&str>,
|
||||
explicit_target: bool,
|
||||
exclude_dirs: &[std::path::PathBuf],
|
||||
force: bool,
|
||||
verbose: bool,
|
||||
workspace_root: Option<&Path>,
|
||||
cancel: &Arc<AtomicBool>,
|
||||
rep: &mut dyn BuildReporter,
|
||||
dependency_stack: &mut Vec<PathBuf>,
|
||||
) -> Result<(), String> {
|
||||
if cancel.load(Ordering::SeqCst) {
|
||||
return Err("Build interrupted".to_string());
|
||||
@@ -344,12 +438,43 @@ fn build_project_at(
|
||||
}
|
||||
let project_name = get_config_str(&config, "package.name");
|
||||
let project_version = get_config_str(&config, "package.version");
|
||||
let host_target = default_target_triple();
|
||||
let build_target_config = get_build_string_with_profile(&config, "target", profile);
|
||||
let build_target = target.or(if build_target_config.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(build_target_config.as_str())
|
||||
});
|
||||
let has_explicit_target = explicit_target || !build_target_config.is_empty();
|
||||
let explicit_build_target = explicit_target
|
||||
.then_some(target)
|
||||
.flatten()
|
||||
.filter(|t| !t.trim().is_empty())
|
||||
.map(|t| {
|
||||
let normalized = normalize_target_os(t);
|
||||
if normalized.is_empty() {
|
||||
host_target.clone()
|
||||
} else {
|
||||
normalized.to_string()
|
||||
}
|
||||
});
|
||||
let configured_build_target = || {
|
||||
if build_target_config.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let normalized = normalize_target_os(&build_target_config);
|
||||
if normalized.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(normalized.to_string())
|
||||
}
|
||||
}
|
||||
};
|
||||
let implicit_build_target = || {
|
||||
target
|
||||
.filter(|t| !t.trim().is_empty())
|
||||
.map(|t| normalize_target_os(t).to_string())
|
||||
};
|
||||
let build_target_owned = explicit_build_target
|
||||
.or_else(configured_build_target)
|
||||
.or_else(implicit_build_target)
|
||||
.unwrap_or(host_target);
|
||||
let build_target = Some(build_target_owned.as_str());
|
||||
let build_language = get_language_with_profile(&config, profile)?;
|
||||
let build_qt = get_bool_with_profile(&config, "qt", profile, false);
|
||||
let lang_key = primary_language(&build_language);
|
||||
@@ -493,68 +618,14 @@ fn build_project_at(
|
||||
let resolved_archiver = resolve_tool("DCR_AR", tc_ar.as_deref());
|
||||
|
||||
let out_dir_config = get_build_string_with_profile(&config, "out_dir", profile);
|
||||
let target_dir_binding = if !out_dir_config.is_empty() {
|
||||
let p = Path::new(&out_dir_config);
|
||||
if p.is_absolute() {
|
||||
out_dir_config
|
||||
} else {
|
||||
project_root.join(p).to_string_lossy().to_string()
|
||||
}
|
||||
} else {
|
||||
match workspace_root {
|
||||
Some(root) => {
|
||||
let target_str = build_target
|
||||
.map(normalize_target_os)
|
||||
.filter(|t| !t.is_empty());
|
||||
let rel: PathBuf = match target_str {
|
||||
Some(ref t) => Path::new("target").join(t).join(profile),
|
||||
None => {
|
||||
let default_dir = if cfg!(target_os = "linux") {
|
||||
let arch = std::env::consts::ARCH;
|
||||
format!("{arch}-unknown-linux-gnu/{profile}")
|
||||
} else if cfg!(any(
|
||||
target_os = "freebsd",
|
||||
target_os = "openbsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "dragonfly"
|
||||
)) {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let os = std::env::consts::OS;
|
||||
format!("{arch}-unknown-{os}/{profile}")
|
||||
} else {
|
||||
profile.to_string()
|
||||
};
|
||||
Path::new("target").join(&default_dir)
|
||||
}
|
||||
};
|
||||
root.join(&rel).to_string_lossy().to_string()
|
||||
}
|
||||
None => {
|
||||
let base_dir =
|
||||
if let Some(rel) = normalize_target(build_target.unwrap_or(""), profile) {
|
||||
PathBuf::from(rel)
|
||||
} else {
|
||||
let default_dir = if cfg!(target_os = "linux") {
|
||||
let arch = std::env::consts::ARCH;
|
||||
format!("{arch}-unknown-linux-gnu/{profile}")
|
||||
} else if cfg!(any(
|
||||
target_os = "freebsd",
|
||||
target_os = "openbsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "dragonfly"
|
||||
)) {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let os = std::env::consts::OS;
|
||||
format!("{arch}-unknown-{os}/{profile}")
|
||||
} else {
|
||||
profile.to_string()
|
||||
};
|
||||
Path::new("target").join(&default_dir)
|
||||
};
|
||||
project_root.join(base_dir).to_string_lossy().to_string()
|
||||
}
|
||||
}
|
||||
};
|
||||
let target_dir_binding = resolve_artifact_target_dir(
|
||||
project_root,
|
||||
workspace_root,
|
||||
profile,
|
||||
&build_target_owned,
|
||||
&out_dir_config,
|
||||
has_explicit_target,
|
||||
);
|
||||
let target_dir = if target_dir_binding.is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -612,24 +683,9 @@ fn build_project_at(
|
||||
let target_str = build_target
|
||||
.map(normalize_target_os)
|
||||
.filter(|t| !t.is_empty());
|
||||
let default_dir = if cfg!(target_os = "linux") {
|
||||
let arch = std::env::consts::ARCH;
|
||||
format!("{arch}-unknown-linux-gnu/{profile}")
|
||||
} else if cfg!(any(
|
||||
target_os = "freebsd",
|
||||
target_os = "openbsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "dragonfly"
|
||||
)) {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let os = std::env::consts::OS;
|
||||
format!("{arch}-unknown-{os}/{profile}")
|
||||
} else {
|
||||
profile.to_string()
|
||||
};
|
||||
let rel = match target_str {
|
||||
Some(ref t) => Path::new("target").join(t).join(profile),
|
||||
None => Path::new("target").join(&default_dir),
|
||||
None => crate::utils::build::native_host_target_rel(profile),
|
||||
};
|
||||
let dep_build_lib1 = dep_member.path.join(&rel);
|
||||
let dep_build_lib2 = wroot.join(&rel);
|
||||
@@ -660,65 +716,63 @@ fn build_project_at(
|
||||
}
|
||||
}
|
||||
|
||||
let deps_table = config.get("dependencies").and_then(|v| v.as_table());
|
||||
let mut resolved = resolve_deps(&config, profile, build_target, project_root)?;
|
||||
resolved.include_dirs.extend(ws_include_dirs);
|
||||
resolved.lib_dirs.extend(ws_lib_dirs);
|
||||
resolved.libs.extend(ws_libs);
|
||||
|
||||
// Registry dependencies are cached under the DCR registry root. Build
|
||||
// them as normal DCR projects before the current project is linked.
|
||||
if let Some(deps) = deps_table {
|
||||
for (name, value) in deps {
|
||||
if register::is_registry_dep(value) {
|
||||
let pkg_info = register::resolve_package_from_registry(name)?;
|
||||
let version = pkg_info
|
||||
.get("latest_version")
|
||||
.or_else(|| pkg_info.get("version"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown");
|
||||
let dep_root = register::package_root_from_registry_info(&pkg_info)?;
|
||||
let include_dir = dep_root.join("target").join("include");
|
||||
let lib_dir = dep_root.join("target").join("lib");
|
||||
|
||||
if !include_dir.exists() || !lib_dir.exists() {
|
||||
rep.on_event(BuildEvent::DepBuilding { name, version });
|
||||
if !dep_root.join("dcr.toml").is_file() {
|
||||
return Err(format!(
|
||||
"Registry dependency `{}` is missing dcr.toml at {}",
|
||||
name,
|
||||
dep_root.display()
|
||||
));
|
||||
}
|
||||
build_project_at(
|
||||
&dep_root,
|
||||
profile,
|
||||
build_target,
|
||||
&[],
|
||||
force,
|
||||
verbose,
|
||||
None,
|
||||
cancel,
|
||||
&mut *rep,
|
||||
)?;
|
||||
rep.on_event(BuildEvent::DepReady {
|
||||
name,
|
||||
version,
|
||||
rebuilt: true,
|
||||
});
|
||||
} else {
|
||||
rep.on_event(BuildEvent::DepReady {
|
||||
name,
|
||||
version,
|
||||
rebuilt: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
let mut built_roots = std::collections::HashSet::new();
|
||||
let mut transitive_roots = std::collections::HashSet::new();
|
||||
let direct_package_roots = resolved.package_roots.clone();
|
||||
for dep_root in &direct_package_roots {
|
||||
if !built_roots.insert(dep_root.clone()) {
|
||||
continue;
|
||||
}
|
||||
let dep_root = Path::new(dep_root);
|
||||
let dep_config =
|
||||
Config::open(&dep_root.join("dcr.toml").to_string_lossy()).map_err(|_| {
|
||||
format!(
|
||||
"DCR dependency is missing dcr.toml at {}",
|
||||
dep_root.display()
|
||||
)
|
||||
})?;
|
||||
let name = get_config_str(&dep_config, "package.name");
|
||||
let version = get_config_str(&dep_config, "package.version");
|
||||
rep.on_event(BuildEvent::DepBuilding {
|
||||
name: &name,
|
||||
version: &version,
|
||||
});
|
||||
build_project_at_with_stack(
|
||||
dep_root,
|
||||
profile,
|
||||
build_target,
|
||||
has_explicit_target,
|
||||
&[],
|
||||
force,
|
||||
verbose,
|
||||
None,
|
||||
cancel,
|
||||
&mut *rep,
|
||||
dependency_stack,
|
||||
)?;
|
||||
rep.on_event(BuildEvent::DepReady {
|
||||
name: &name,
|
||||
version: &version,
|
||||
rebuilt: true,
|
||||
});
|
||||
extend_transitive_link_deps(
|
||||
dep_root,
|
||||
profile,
|
||||
build_target,
|
||||
&mut resolved,
|
||||
&mut transitive_roots,
|
||||
)?;
|
||||
}
|
||||
|
||||
let (resolved_cflags, resolved_ldflags) =
|
||||
let (mut resolved_cflags, mut resolved_ldflags) =
|
||||
resolve_pkg_config_flags(&pkg_configs, &build_cflags, &build_ldflags)?;
|
||||
resolved_cflags.extend(resolved.cflags.clone());
|
||||
resolved_ldflags.extend(resolved.ldflags.clone());
|
||||
let mut combined_excludes = Vec::new();
|
||||
for dir in exclude_dirs {
|
||||
combined_excludes.push(dir.clone());
|
||||
@@ -937,6 +991,43 @@ fn build_project_at(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds link flags needed by dependencies of a static DCR library.
|
||||
/// Static archives do not contain their own dependency objects, so the final
|
||||
/// consumer must link every transitive package in dependency order.
|
||||
fn extend_transitive_link_deps(
|
||||
package_root: &Path,
|
||||
profile: &str,
|
||||
target: Option<&str>,
|
||||
consumer_deps: &mut crate::core::deps::common::ResolvedDeps,
|
||||
visited: &mut std::collections::HashSet<PathBuf>,
|
||||
) -> Result<(), String> {
|
||||
let root = package_root
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| package_root.to_path_buf());
|
||||
if !visited.insert(root.clone()) {
|
||||
return Ok(());
|
||||
}
|
||||
let config =
|
||||
Config::open(&root.join("dcr.toml").to_string_lossy()).map_err(|err| err.to_string())?;
|
||||
let deps = resolve_deps(&config, profile, target, &root)?;
|
||||
consumer_deps.include_dirs.extend(deps.include_dirs);
|
||||
consumer_deps.lib_dirs.extend(deps.lib_dirs);
|
||||
consumer_deps.libs.extend(deps.libs);
|
||||
consumer_deps.cflags.extend(deps.cflags);
|
||||
consumer_deps.ldflags.extend(deps.ldflags);
|
||||
for child_root in deps.package_roots {
|
||||
extend_transitive_link_deps(
|
||||
Path::new(&child_root),
|
||||
profile,
|
||||
target,
|
||||
consumer_deps,
|
||||
visited,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Converts relative paths in flags to absolute paths based on project root for include and link flags.
|
||||
fn absolutize_flag_path(flag: &str, project_root: &Path, prefixes: &[&str]) -> String {
|
||||
for prefix in prefixes {
|
||||
if let Some(rest) = flag.strip_prefix(prefix) {
|
||||
@@ -957,6 +1048,7 @@ fn absolutize_flag_path(flag: &str, project_root: &Path, prefixes: &[&str]) -> S
|
||||
flag.to_string()
|
||||
}
|
||||
|
||||
/// Finds the nearest ancestor directory named "target" starting from the given dir.
|
||||
fn find_target_root(dir: &str, fallback: &Path) -> PathBuf {
|
||||
let p = Path::new(dir);
|
||||
for ancestor in p.ancestors() {
|
||||
@@ -967,6 +1059,7 @@ fn find_target_root(dir: &str, fallback: &Path) -> PathBuf {
|
||||
fallback.to_path_buf()
|
||||
}
|
||||
|
||||
/// Packages the built library by copying headers and libs to the target include and lib directories.
|
||||
fn package_library(
|
||||
ctx: &BuildContext,
|
||||
headers: &[PathBuf],
|
||||
@@ -1021,12 +1114,14 @@ fn package_library(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Holds the resolved executable paths for Qt tools.
|
||||
pub(crate) struct ToolchainExecs {
|
||||
pub(crate) uic: String,
|
||||
pub(crate) moc: String,
|
||||
pub(crate) rcc: String,
|
||||
}
|
||||
|
||||
/// Resolves the paths for uic, moc, and rcc tools, preferring configured values or pkg-config/Qt6 variants.
|
||||
fn resolve_toolchain_execs(
|
||||
uic: &Option<String>,
|
||||
moc: &Option<String>,
|
||||
@@ -1041,6 +1136,7 @@ fn resolve_toolchain_execs(
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves a Qt tool path, falling back to Qt6 variants if not found in standard locations.
|
||||
fn resolve_qt_tool(configured: &Option<String>, qt_bins: Option<&Path>, tool: &str) -> String {
|
||||
if let Some(value) = configured {
|
||||
return value.clone();
|
||||
@@ -1063,6 +1159,7 @@ fn resolve_qt_tool(configured: &Option<String>, qt_bins: Option<&Path>, tool: &s
|
||||
tool.to_string()
|
||||
}
|
||||
|
||||
/// Queries pkg-config for Qt6 host bin directories for the given packages.
|
||||
fn resolve_qt_host_bins(pkgs: &[String]) -> Option<PathBuf> {
|
||||
let qt_pkgs: Vec<&String> = pkgs.iter().filter(|p| p.starts_with("Qt6")).collect();
|
||||
if qt_pkgs.is_empty() {
|
||||
@@ -1098,6 +1195,7 @@ fn resolve_qt_host_bins(pkgs: &[String]) -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Executes pkg-config to get a variable value for a package and returns the path if valid.
|
||||
fn query_pkg_config_var(pkg: &str, var: &str) -> Option<PathBuf> {
|
||||
let output = std::process::Command::new("pkg-config")
|
||||
.arg(format!("--variable={var}"))
|
||||
@@ -1115,6 +1213,7 @@ fn query_pkg_config_var(pkg: &str, var: &str) -> Option<PathBuf> {
|
||||
if path.is_dir() { Some(path) } else { None }
|
||||
}
|
||||
|
||||
/// Extracts tool binaries from a libexec directory or its bin subdirectory.
|
||||
fn qt_bins_from_libexec(libexec: &Path) -> Option<PathBuf> {
|
||||
for tool in ["moc", "uic", "rcc"] {
|
||||
if libexec.join(tool).is_file() {
|
||||
@@ -1128,12 +1227,14 @@ fn qt_bins_from_libexec(libexec: &Path) -> Option<PathBuf> {
|
||||
if bin.is_dir() { Some(bin) } else { None }
|
||||
}
|
||||
|
||||
/// Detects if a Qt6 tool variant (like uic6 or uic-qt6) is available on the PATH.
|
||||
fn detect_qt6_tool_variant(tool: &str) -> Option<String> {
|
||||
[format!("{tool}6"), format!("{tool}-qt6")]
|
||||
.into_iter()
|
||||
.find(|candidate| is_on_path(candidate))
|
||||
}
|
||||
|
||||
/// Checks if a command is available either as a file or in the PATH environment variable.
|
||||
fn is_on_path(cmd: &str) -> bool {
|
||||
if Path::new(cmd).is_file() {
|
||||
return true;
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::builder::artifact;
|
||||
use crate::core::build::common;
|
||||
use crate::core::build::{builder::BuildContext, builder::artifact, common};
|
||||
use crate::utils::build::{is_compile_only, is_flat_bin};
|
||||
use std::path::Path;
|
||||
use std::time::Instant;
|
||||
use std::{path::Path, time::Instant};
|
||||
|
||||
/// Keeps only assembler-relevant flags from a cflags list.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `flags`: Full compiler flag list (often shared with C/C++).
|
||||
///
|
||||
/// # Returns
|
||||
/// Cloned flags that start with `-I`/`-i` or equal `-g`.
|
||||
pub fn filter_asm_flags(flags: &[String]) -> Vec<String> {
|
||||
flags
|
||||
.iter()
|
||||
@@ -16,6 +20,19 @@ pub fn filter_asm_flags(flags: &[String]) -> Vec<String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Builds assembly sources with direct flat emission when `kind` is `flat-bin`.
|
||||
///
|
||||
/// For non-flat kinds, assembles to objects and then links/archives as usual.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `ctx`: Build context (kind, sources, flags, …).
|
||||
/// - `backend_name`: Label used in error messages (e.g. `"NASM"`).
|
||||
/// - `default_assembler`: Assembler binary when `ctx.compiler` is empty.
|
||||
/// - `extensions`: Source extensions to collect.
|
||||
/// - `build_object`: Per-source assemble callback `(assembler, src, out, ctx)`.
|
||||
///
|
||||
/// # Returns
|
||||
/// Elapsed seconds, or an error string.
|
||||
pub fn build_assembly<F>(
|
||||
ctx: &BuildContext,
|
||||
backend_name: &str,
|
||||
@@ -36,6 +53,15 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds assembly sources; for `flat-bin`, assembles to objects then runs objcopy.
|
||||
///
|
||||
/// For non-flat kinds, assembles to objects and then links/archives as usual.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - Same as [`build_assembly`]; only the flat emission strategy differs.
|
||||
///
|
||||
/// # Returns
|
||||
/// Elapsed seconds, or an error string.
|
||||
pub fn build_assembly_via_objcopy<F>(
|
||||
ctx: &BuildContext,
|
||||
backend_name: &str,
|
||||
@@ -56,12 +82,14 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
/// Flat-bin emission strategy: direct assembler output, or object then objcopy.
|
||||
#[derive(Clone, Copy)]
|
||||
enum FlatEmit {
|
||||
Direct,
|
||||
ViaObjcopy,
|
||||
}
|
||||
|
||||
/// Core assembly build with a chosen flat emission mode; `build_object` runs per source in parallel.
|
||||
fn build_assembly_with_mode<F>(
|
||||
ctx: &BuildContext,
|
||||
backend_name: &str,
|
||||
@@ -74,6 +102,7 @@ where
|
||||
F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync,
|
||||
{
|
||||
let lang = ctx.language.to_lowercase();
|
||||
// Validate language
|
||||
if lang.split(',').any(|p| {
|
||||
let t = p.trim();
|
||||
t != "asm" && t != "llvm_ir" && t != "llvm-ir" && t != "ll"
|
||||
@@ -104,6 +133,7 @@ where
|
||||
};
|
||||
|
||||
let objects = match (flat, flat_mode) {
|
||||
// Dispatch based on flat binary kind and emission mode
|
||||
(true, FlatEmit::Direct) => {
|
||||
let outs: Vec<String> = sources
|
||||
.iter()
|
||||
@@ -148,6 +178,7 @@ where
|
||||
artifact::link_binary(ctx, &objects, "cc", start_time)
|
||||
}
|
||||
|
||||
/// Compiles all sources to objects in parallel (non-flat path) and returns object paths.
|
||||
fn build_objects_common<F>(
|
||||
assembler: &str,
|
||||
sources: &[String],
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::common;
|
||||
use crate::core::build::language::asm::common as asm;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use crate::core::build::{builder::BuildContext, common};
|
||||
use std::{fs, path::Path, process::Command};
|
||||
|
||||
/// Builds an assembly file using the FASM assembler.
|
||||
///
|
||||
/// Delegates to the shared assembly build logic with FASM configuration.
|
||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
asm::build_assembly(ctx, "FASM", "fasm", &["asm", "fasm"], build_object)
|
||||
}
|
||||
|
||||
/// Collects all source files for assembly files using FASM.
|
||||
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
common::collect_sources(
|
||||
ctx.source_roots,
|
||||
@@ -18,6 +19,7 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
|
||||
)
|
||||
}
|
||||
|
||||
/// Assembles one FASM source into an object file.
|
||||
fn build_object(
|
||||
assembler: &str,
|
||||
source: &str,
|
||||
@@ -28,6 +30,7 @@ fn build_object(
|
||||
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
|
||||
}
|
||||
|
||||
// Skip rebuilding if the object is up to date
|
||||
if !common::needs_rebuild(source, obj_path) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -15,13 +15,14 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::common;
|
||||
use crate::core::build::language::asm::common as asm;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use crate::core::build::{builder::BuildContext, common};
|
||||
use std::{fs, path::Path, process::Command};
|
||||
|
||||
/// Builds GAS (`.s`) sources via the GNU Assembler.
|
||||
///
|
||||
/// Resolves `as` on PATH and delegates to the shared assembly builder. For
|
||||
/// `flat-bin`, intermediates are converted with objcopy after assembly.
|
||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
// Use full path to as
|
||||
let as_path = std::process::Command::new("which")
|
||||
@@ -35,6 +36,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
asm::build_assembly_via_objcopy(ctx, "GAS", &as_path, &["s"], build_object)
|
||||
}
|
||||
|
||||
/// Collects assembly source files with .s extension from the build context's source roots.
|
||||
///
|
||||
/// Excludes specified directories and respects include paths for filtering.
|
||||
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
common::collect_sources(
|
||||
ctx.source_roots,
|
||||
@@ -44,6 +48,10 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
|
||||
)
|
||||
}
|
||||
|
||||
/// Assembles a single .s source file into an object file using the provided assembler.
|
||||
///
|
||||
/// Creates parent directories for the output object, skips if up-to-date,
|
||||
/// runs the assembler with flags from the build context, and handles verbose output.
|
||||
fn build_object(
|
||||
assembler: &str,
|
||||
source: &str,
|
||||
@@ -54,10 +62,12 @@ fn build_object(
|
||||
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
|
||||
}
|
||||
|
||||
// Check if the object file is already up to date to avoid unnecessary rebuilds.
|
||||
if !common::needs_rebuild(source, obj_path) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Prepare the assembler command with source file and output path.
|
||||
let mut cmd = Command::new(assembler);
|
||||
cmd.arg(source).arg("-o").arg(obj_path);
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::common;
|
||||
use crate::core::build::language::asm::common as asm;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use crate::core::build::{builder::BuildContext, common};
|
||||
use std::{fs, path::Path, process::Command};
|
||||
|
||||
/// Builds MASM (`.asm`) sources via `ml`.
|
||||
///
|
||||
/// For `flat-bin`, intermediates are converted with objcopy after assembly.
|
||||
/// Returns elapsed seconds or an error.
|
||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
asm::build_assembly_via_objcopy(ctx, "MASM", "ml", &["asm"], build_object)
|
||||
}
|
||||
|
||||
/// Collects MASM assembly source files from the given source roots.
|
||||
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
common::collect_sources(
|
||||
ctx.source_roots,
|
||||
@@ -18,6 +20,12 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds a single MASM object file from the given source.
|
||||
///
|
||||
/// Creates the parent directory for the output object if it doesn't exist,
|
||||
/// skips the build if the object is already up to date, constructs the ml
|
||||
/// command line with flags, prints the command in debug mode if enabled,
|
||||
/// and executes it.
|
||||
fn build_object(
|
||||
assembler: &str,
|
||||
source: &str,
|
||||
@@ -25,10 +33,12 @@ fn build_object(
|
||||
ctx: &BuildContext,
|
||||
) -> Result<(), String> {
|
||||
if let Some(parent) = Path::new(obj_path).parent() {
|
||||
// Ensure the parent directory exists for the object file
|
||||
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
|
||||
}
|
||||
|
||||
if !common::needs_rebuild(source, obj_path) {
|
||||
// Skip rebuild if the object file is already current
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -40,6 +50,7 @@ fn build_object(
|
||||
.arg(source);
|
||||
|
||||
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
|
||||
// Add filtered compiler flags to the command
|
||||
cmd.arg(flag);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/// Assembly language module for the DCR build system.
|
||||
pub mod common;
|
||||
pub mod fasm;
|
||||
pub mod gas;
|
||||
@@ -23,15 +24,21 @@ pub mod nasm;
|
||||
|
||||
use crate::core::build::language::Language;
|
||||
|
||||
/// Represents the assembly language support in the build system.
|
||||
pub struct Asm;
|
||||
|
||||
impl Language for Asm {
|
||||
/// Returns the identifier string for this language.
|
||||
fn id(&self) -> &'static str {
|
||||
"asm"
|
||||
}
|
||||
|
||||
/// Returns the list of supported file extensions for assembly files.
|
||||
fn extensions(&self) -> &'static [&'static str] {
|
||||
&["s", "S", "asm"]
|
||||
}
|
||||
|
||||
/// Checks whether the given token matches the assembly language, using case-insensitive comparison.
|
||||
fn matches_token(&self, token: &str) -> bool {
|
||||
token.eq_ignore_ascii_case("asm")
|
||||
}
|
||||
|
||||
@@ -15,17 +15,21 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::common;
|
||||
/// Support for NASM assembly language in the DCR build system.
|
||||
use crate::core::build::language::asm::common as asm;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use crate::core::build::{builder::BuildContext, common};
|
||||
use std::{fs, path::Path, process::Command};
|
||||
|
||||
/// Builds assembly sources using the NASM assembler.
|
||||
///
|
||||
/// Delegates to the shared assembly builder with NASM-specific configuration.
|
||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
asm::build_assembly(ctx, "NASM", "nasm", &["asm"], build_object)
|
||||
}
|
||||
|
||||
/// Collects source files for NASM assembly.
|
||||
///
|
||||
/// Filters for .asm files only, as NASM uses that extension (distinct from GAS .s).
|
||||
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
// NASM handles .asm only (not .s which is GAS syntax, not .S which is GCC preprocessed)
|
||||
common::collect_sources(
|
||||
@@ -36,6 +40,9 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds a single object file from an assembly source using NASM.
|
||||
///
|
||||
/// Handles directory creation, rebuild checks, format selection, and command execution.
|
||||
fn build_object(
|
||||
assembler: &str,
|
||||
source: &str,
|
||||
@@ -43,13 +50,16 @@ fn build_object(
|
||||
ctx: &BuildContext,
|
||||
) -> Result<(), String> {
|
||||
if let Some(parent) = Path::new(obj_path).parent() {
|
||||
// Ensure output directory exists before building.
|
||||
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
|
||||
}
|
||||
|
||||
// Skip building if the object is already up to date.
|
||||
if !common::needs_rebuild(source, obj_path) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Select appropriate object format for the target platform and binary type.
|
||||
let format = if crate::utils::build::is_flat_bin(ctx.kind) {
|
||||
"bin"
|
||||
} else {
|
||||
@@ -63,6 +73,7 @@ fn build_object(
|
||||
.arg("-o")
|
||||
.arg(obj_path);
|
||||
|
||||
// Append any additional assembly flags from build context.
|
||||
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
|
||||
cmd.arg(flag);
|
||||
}
|
||||
@@ -74,6 +85,9 @@ fn build_object(
|
||||
common::run_command_sync_output(&mut cmd)
|
||||
}
|
||||
|
||||
/// Selects the NASM object file format string based on the target platform.
|
||||
///
|
||||
/// Supports different formats for x86 on various OSes and architectures.
|
||||
fn nasm_format(platform: Option<&str>) -> &'static str {
|
||||
if let Some(p) = platform {
|
||||
let p = p.to_lowercase().replace('-', "_");
|
||||
|
||||
@@ -17,15 +17,21 @@
|
||||
|
||||
use crate::core::build::language::Language;
|
||||
|
||||
/// Represents the C programming language support within the DCR build system.
|
||||
pub struct C;
|
||||
|
||||
impl Language for C {
|
||||
/// Returns the unique identifier for the C language.
|
||||
fn id(&self) -> &'static str {
|
||||
"c"
|
||||
}
|
||||
|
||||
/// Returns an array containing the file extension associated with C source code.
|
||||
fn extensions(&self) -> &'static [&'static str] {
|
||||
&["c"]
|
||||
}
|
||||
|
||||
/// Checks whether the given token matches the C language identifier in a case-insensitive manner.
|
||||
fn matches_token(&self, token: &str) -> bool {
|
||||
token.eq_ignore_ascii_case("c")
|
||||
}
|
||||
|
||||
@@ -15,20 +15,26 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/// C++ language support for the DCR build system.
|
||||
pub mod qt;
|
||||
|
||||
use crate::core::build::language::Language;
|
||||
|
||||
pub struct Cxx;
|
||||
|
||||
/// C++ language implementation.
|
||||
impl Language for Cxx {
|
||||
/// Returns the language ID "cxx".
|
||||
fn id(&self) -> &'static str {
|
||||
"cxx"
|
||||
}
|
||||
/// Returns the extensions for C++ files.
|
||||
fn extensions(&self) -> &'static [&'static str] {
|
||||
&["cpp", "cxx", "cc"]
|
||||
}
|
||||
/// Checks if the token matches C++ language names, ignoring case.
|
||||
fn matches_token(&self, token: &str) -> bool {
|
||||
// Case-insensitive comparison using to_lowercase
|
||||
matches!(token.to_lowercase().as_str(), "c++" | "cpp" | "cxx")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,22 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::common;
|
||||
use crate::core::{build::builder::BuildContext, build::common};
|
||||
use crate::utils::build::run_pkg_config;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
/// Runs uic/rcc/moc on Qt UI, resource, and header sources under `build_dir/qt`.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `ctx`: Sources, excludes, and toolchain tool paths.
|
||||
/// - `build_dir`: Directory that will contain the generated `qt/` tree.
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Ok(Some(qt_dir))` if any generated files were produced.
|
||||
/// - `Ok(None)` if there was nothing to process.
|
||||
/// - `Err` if a Qt tool fails.
|
||||
pub fn process_qt(ctx: &BuildContext, build_dir: &Path) -> Result<Option<PathBuf>, String> {
|
||||
let qt_dir = build_dir.join("qt");
|
||||
fs::create_dir_all(&qt_dir).map_err(|e| format!("Failed to create qt dir: {e}"))?;
|
||||
@@ -37,6 +46,7 @@ pub fn process_qt(ctx: &BuildContext, build_dir: &Path) -> Result<Option<PathBuf
|
||||
let mut qt_moc_args = Vec::new();
|
||||
let qt_modules = vec!["Qt6Core", "Qt6Widgets", "Qt6Gui", "Qt6Svg"];
|
||||
|
||||
// Collect -I and -D flags from pkg-config for Qt modules to use in moc invocations
|
||||
for module in qt_modules {
|
||||
if let Ok(cflags) = run_pkg_config(module, "--cflags") {
|
||||
for flag in cflags.split_whitespace() {
|
||||
@@ -123,6 +133,7 @@ pub fn process_qt(ctx: &BuildContext, build_dir: &Path) -> Result<Option<PathBuf
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes the specified command with the given arguments and returns success or an error message.
|
||||
fn run_command(cmd_path: &str, args: &[&str], verbose: bool) -> Result<(), String> {
|
||||
let mut cmd = Command::new(cmd_path);
|
||||
cmd.args(args);
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::common;
|
||||
use crate::core::build::language::Language;
|
||||
use crate::core::build::language::asm::common as asm;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use crate::core::build::{builder::BuildContext, common, language::Language};
|
||||
use std::{fs, path::Path, process::Command};
|
||||
|
||||
/// Represents the LLVM IR language implementation for the build system.
|
||||
pub struct LlvmIr;
|
||||
|
||||
impl Language for LlvmIr {
|
||||
/// Returns the unique identifier for the LLVM IR language.
|
||||
fn id(&self) -> &'static str {
|
||||
"llvm_ir"
|
||||
}
|
||||
|
||||
/// Returns the list of file extensions recognized for this language.
|
||||
fn extensions(&self) -> &'static [&'static str] {
|
||||
&["ll"]
|
||||
}
|
||||
|
||||
/// Determines whether the provided token matches the LLVM IR language.
|
||||
fn matches_token(&self, token: &str) -> bool {
|
||||
matches!(token.to_lowercase().as_str(), "llvm_ir" | "llvm-ir" | "ll")
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds LLVM IR source files into object files using llc.
|
||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
asm::build_assembly_via_objcopy(ctx, "LLVM-IR", "llc", &["ll"], build_object)
|
||||
}
|
||||
|
||||
/// Collects source files for LLVM IR compilation from the given context.
|
||||
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
common::collect_sources(
|
||||
ctx.source_roots,
|
||||
@@ -33,6 +37,7 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds a single LLVM IR source file into an object file.
|
||||
fn build_object(
|
||||
assembler: &str,
|
||||
source: &str,
|
||||
@@ -40,6 +45,7 @@ fn build_object(
|
||||
ctx: &BuildContext,
|
||||
) -> Result<(), String> {
|
||||
if let Some(parent) = Path::new(obj_path).parent() {
|
||||
// Create parent directory if it does not exist
|
||||
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,21 +15,29 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/// Core module for language support in the DCR build system.
|
||||
/// Defines the Language trait and helper functions to register and lookup languages.
|
||||
pub mod asm;
|
||||
pub mod c;
|
||||
pub mod cxx;
|
||||
pub mod llvm_ir;
|
||||
|
||||
/// Trait representing a supported programming language.
|
||||
/// Each implementation must specify its unique identifier, list of supported file extensions,
|
||||
/// and a method to determine if a given token matches the language.
|
||||
pub trait Language {
|
||||
fn id(&self) -> &'static str;
|
||||
fn extensions(&self) -> &'static [&'static str];
|
||||
fn matches_token(&self, token: &str) -> bool;
|
||||
}
|
||||
|
||||
/// Returns a fixed-size array containing static references to all supported language types.
|
||||
pub fn languages() -> [&'static dyn Language; 4] {
|
||||
[&c::C, &cxx::Cxx, &asm::Asm, &llvm_ir::LlvmIr]
|
||||
}
|
||||
|
||||
/// Looks up the language implementation that matches the given token by checking each language's
|
||||
/// matches_token method. Returns the first match or None if no language matches.
|
||||
pub fn language_for_token(token: &str) -> Option<&'static dyn Language> {
|
||||
languages().into_iter().find(|l| l.matches_token(token))
|
||||
}
|
||||
@@ -38,6 +46,7 @@ pub fn language_for_token(token: &str) -> Option<&'static dyn Language> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Ensures that input tokens are resolved to the correct language IDs.
|
||||
#[test]
|
||||
fn tokens_resolve_to_languages() {
|
||||
assert_eq!(language_for_token("c").unwrap().id(), "c");
|
||||
@@ -48,6 +57,7 @@ mod tests {
|
||||
assert!(language_for_token("rust").is_none());
|
||||
}
|
||||
|
||||
/// Ensures that each language reports the expected set of file extensions.
|
||||
#[test]
|
||||
fn languages_expose_expected_extensions() {
|
||||
assert_eq!(c::C.extensions(), &["c"]);
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/// Core build module for DCR — a Cargo-like C/C++ project manager.
|
||||
/// This module defines the build engine and re-exports key components for
|
||||
/// building C/C++ projects.
|
||||
#[cfg(feature = "archive")]
|
||||
pub mod archive;
|
||||
pub mod builder;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
|
||||
/// Represents a build request with options for the build process, including profile selection, target specification, force flag, verbosity, workspace, and a cancellation signal.
|
||||
pub struct BuildRequest {
|
||||
pub profile: String,
|
||||
pub target: Option<String>,
|
||||
@@ -30,21 +31,25 @@ pub struct BuildRequest {
|
||||
pub cancel: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
/// Contains the result of a build operation, specifically the duration in seconds.
|
||||
#[allow(dead_code)]
|
||||
pub struct BuildOutcome {
|
||||
pub secs: f64,
|
||||
}
|
||||
|
||||
/// Wraps a string message as a build error.
|
||||
pub struct BuildError {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Creates a BuildError from a String message.
|
||||
impl From<String> for BuildError {
|
||||
fn from(message: String) -> Self {
|
||||
BuildError { message }
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents events that the build engine can emit during compilation and packaging.
|
||||
pub enum BuildEvent<'a> {
|
||||
TargetStart {
|
||||
index: usize,
|
||||
@@ -81,6 +86,8 @@ pub enum BuildEvent<'a> {
|
||||
},
|
||||
}
|
||||
|
||||
/// Trait for handling build events in different reporting backends.
|
||||
pub trait BuildReporter {
|
||||
/// Callback invoked when a build event is generated.
|
||||
fn on_event(&mut self, event: BuildEvent<'_>);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
use crate::core::build_config::Config;
|
||||
use crate::utils::build::{get_config_str, normalize_target_os, profile_table};
|
||||
|
||||
/// Retrieves a raw TOML value from the config using a prioritized lookup order based on target, profile, and base sections.
|
||||
///
|
||||
/// The keys are checked in this order:
|
||||
/// {section}.{target}.{profile}.{field}, {section}.{profile}.{target}.{field}, {section}.{target}.{field}, {section}.{profile}.{field}, {section}.{field}
|
||||
///
|
||||
/// Returns the value if found, otherwise None.
|
||||
pub(crate) fn get_config_value_raw(
|
||||
config: &Config,
|
||||
section: &str,
|
||||
@@ -57,6 +63,9 @@ pub(crate) fn get_config_value_raw(
|
||||
None
|
||||
}
|
||||
|
||||
/// Checks if the 'inherit' flag is enabled for the given section, profile, and target.
|
||||
///
|
||||
/// Returns true by default if the flag is not set.
|
||||
pub(crate) fn get_inherit(
|
||||
config: &Config,
|
||||
section: &str,
|
||||
@@ -68,6 +77,9 @@ pub(crate) fn get_inherit(
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
/// Extracts a trimmed non-empty string value from the config.
|
||||
///
|
||||
/// Returns Some(value) if found, None otherwise.
|
||||
pub(crate) fn get_config_value(
|
||||
config: &Config,
|
||||
section: &str,
|
||||
@@ -80,6 +92,9 @@ pub(crate) fn get_config_value(
|
||||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
/// Fetches a string value from the build section, inheriting from base if enabled.
|
||||
///
|
||||
/// Returns the value or default empty string if not inheriting.
|
||||
pub(crate) fn get_string_with_profile_and_target(
|
||||
config: &Config,
|
||||
field: &str,
|
||||
@@ -94,10 +109,12 @@ pub(crate) fn get_string_with_profile_and_target(
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a build string value for the given profile without a target.
|
||||
pub fn get_build_string_with_profile(config: &Config, field: &str, profile: &str) -> String {
|
||||
get_string_with_profile_and_target(config, field, profile, None)
|
||||
}
|
||||
|
||||
/// Gets a string value from the language-specific build.{lang} section.
|
||||
pub(crate) fn get_lang_string(
|
||||
config: &Config,
|
||||
lang: &str,
|
||||
@@ -108,6 +125,7 @@ pub(crate) fn get_lang_string(
|
||||
get_config_value(config, &format!("build.{lang}"), field, profile, target)
|
||||
}
|
||||
|
||||
/// Gets a list of strings from the language-specific build.{lang} section.
|
||||
pub(crate) fn get_lang_list(
|
||||
config: &Config,
|
||||
lang: &str,
|
||||
@@ -121,6 +139,7 @@ pub(crate) fn get_lang_list(
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a TOML array into a Vec<String>, erroring if not an array of strings.
|
||||
pub(crate) fn parse_string_array(value: &toml::Value, key: &str) -> Result<Vec<String>, String> {
|
||||
let arr = value
|
||||
.as_array()
|
||||
@@ -135,6 +154,9 @@ pub(crate) fn parse_string_array(value: &toml::Value, key: &str) -> Result<Vec<S
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Collects a list of strings from config, supporting inheritance from base and profile/target overrides.
|
||||
///
|
||||
/// If inherit is true, starts with the base list and appends custom entries if defined.
|
||||
pub(crate) fn get_list_with_profile_and_target(
|
||||
config: &Config,
|
||||
field: &str,
|
||||
@@ -154,6 +176,7 @@ pub(crate) fn get_list_with_profile_and_target(
|
||||
.get(&format!("build.{field}"))
|
||||
.map(|v| v == &val)
|
||||
.unwrap_or(false);
|
||||
// Skip re-adding the base list when this is the base definition to prevent duplication
|
||||
if !is_base {
|
||||
let custom = parse_string_array(&val, &format!("build.{field}"))?;
|
||||
if inherit {
|
||||
@@ -184,6 +207,7 @@ pub(crate) fn get_list_with_profile_and_target(
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Gets the list of strings for the given profile without a target.
|
||||
pub(crate) fn get_list_with_profile(
|
||||
config: &Config,
|
||||
field: &str,
|
||||
@@ -192,10 +216,21 @@ pub(crate) fn get_list_with_profile(
|
||||
get_list_with_profile_and_target(config, field, profile, None)
|
||||
}
|
||||
|
||||
/// Returns the list of targets for the given profile.
|
||||
pub(crate) fn get_targets(config: &Config, profile: &str) -> Result<Vec<String>, String> {
|
||||
get_list_with_profile(config, "targets", profile)
|
||||
}
|
||||
|
||||
/// Parses the config value as a list of strings.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `config`: Project config.
|
||||
/// - `key`: Dot-path key (e.g. `build.sources`).
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Ok([])` if the key is missing.
|
||||
/// - `Ok(vec)` for an array of strings.
|
||||
/// - `Err` if present but not an array of strings.
|
||||
pub(crate) fn get_config_list(config: &Config, key: &str) -> Result<Vec<String>, String> {
|
||||
let value = match config.get(key) {
|
||||
Some(v) => v,
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::build::engine::ToolchainExecs;
|
||||
use crate::core::build_config::Config;
|
||||
use crate::core::{build::engine::ToolchainExecs, build_config::Config};
|
||||
use crate::utils::build::{VersionInfo, profile_table, substitute_vars};
|
||||
use glob::glob;
|
||||
use std::fs;
|
||||
@@ -30,6 +29,7 @@ pub(crate) struct BuildStep {
|
||||
cmd: String,
|
||||
}
|
||||
|
||||
/// Holds interpolated version and profile strings for build step substitution.
|
||||
pub(crate) struct StepVars<'a> {
|
||||
pub(crate) profile: &'a str,
|
||||
pub(crate) version: &'a str,
|
||||
@@ -40,6 +40,7 @@ pub(crate) struct StepVars<'a> {
|
||||
pub(crate) version_suffix_dash: &'a str,
|
||||
}
|
||||
|
||||
/// Parses TOML array of build step tables into Vec<BuildStep>.
|
||||
fn get_build_steps_from_value(value: &toml::Value, key: &str) -> Result<Vec<BuildStep>, String> {
|
||||
let arr = value
|
||||
.as_array()
|
||||
@@ -86,6 +87,7 @@ fn get_build_steps_from_value(value: &toml::Value, key: &str) -> Result<Vec<Buil
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Retrieves build steps for a specific profile from config, falling back to default if not present.
|
||||
pub(crate) fn get_build_steps_with_profile(
|
||||
config: &Config,
|
||||
field: &str,
|
||||
@@ -99,6 +101,7 @@ pub(crate) fn get_build_steps_with_profile(
|
||||
get_build_steps(config, &format!("build.{field}"))
|
||||
}
|
||||
|
||||
/// Gets build steps from config for the given key.
|
||||
fn get_build_steps(config: &Config, key: &str) -> Result<Vec<BuildStep>, String> {
|
||||
let value = match config.get(key) {
|
||||
Some(v) => v,
|
||||
@@ -110,6 +113,7 @@ fn get_build_steps(config: &Config, key: &str) -> Result<Vec<BuildStep>, String>
|
||||
use crate::core::build::report::{BuildEvent, BuildReporter};
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
|
||||
/// Runs all build steps in sequence, checking for cancellation and reporting progress.
|
||||
pub(crate) fn run_build_steps(
|
||||
steps: &[BuildStep],
|
||||
tools: &ToolchainExecs,
|
||||
@@ -127,6 +131,7 @@ pub(crate) fn run_build_steps(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Executes a single build step, handling glob expansion, output paths, and command substitution.
|
||||
fn run_build_step(
|
||||
step: &BuildStep,
|
||||
tools: &ToolchainExecs,
|
||||
@@ -137,6 +142,7 @@ fn run_build_step(
|
||||
) -> Result<(), String> {
|
||||
let input_pattern = expand_step_value(&step.input, "", vars);
|
||||
let inputs = expand_glob(&input_pattern)?;
|
||||
// Skip step if no matching input files found
|
||||
if inputs.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -170,6 +176,7 @@ fn run_build_step(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes files (not directories) matching the given glob patterns.
|
||||
pub(crate) fn clean_generated_files(patterns: &[String]) -> Result<(), String> {
|
||||
for pattern in patterns {
|
||||
for path in expand_glob(pattern)? {
|
||||
@@ -181,6 +188,7 @@ pub(crate) fn clean_generated_files(patterns: &[String]) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Expands glob patterns to find matching files.
|
||||
pub(crate) fn expand_glob(pattern: &str) -> Result<Vec<PathBuf>, String> {
|
||||
let mut out = Vec::new();
|
||||
let entries = glob(pattern).map_err(|err| format!("glob error: {err}"))?;
|
||||
@@ -191,6 +199,7 @@ pub(crate) fn expand_glob(pattern: &str) -> Result<Vec<PathBuf>, String> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Verifies that expected artifacts are present by expanding patterns.
|
||||
pub(crate) fn verify_expectations(patterns: &[String], vars: &StepVars) -> Result<(), String> {
|
||||
for pattern in patterns {
|
||||
let expanded = expand_step_value(pattern, "", vars);
|
||||
@@ -202,6 +211,7 @@ pub(crate) fn verify_expectations(patterns: &[String], vars: &StepVars) -> Resul
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// True if the step should run: missing/unreadable output, unreadable input, or input newer than output.
|
||||
fn should_run_step(input: &Path, output: &Path) -> bool {
|
||||
let in_time = fs::metadata(input).and_then(|m| m.modified());
|
||||
let out_time = fs::metadata(output).and_then(|m| m.modified());
|
||||
@@ -212,6 +222,7 @@ fn should_run_step(input: &Path, output: &Path) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Substitutes variables into step command template and replaces placeholders.
|
||||
fn substitute_step_cmd(
|
||||
template: &str,
|
||||
input: &Path,
|
||||
@@ -232,6 +243,7 @@ fn substitute_step_cmd(
|
||||
.replace("{stem}", stem)
|
||||
}
|
||||
|
||||
/// Builds a list of compiler flags from cflags, include dirs, and compiler type.
|
||||
pub(crate) fn build_step_flags(
|
||||
cflags: &[String],
|
||||
include_dirs: &[String],
|
||||
@@ -264,6 +276,7 @@ pub(crate) fn build_step_flags(
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
/// Quotes argument if it contains whitespace or double quotes.
|
||||
fn quote_step_arg(arg: String) -> String {
|
||||
if !arg.chars().any(|c| c.is_whitespace() || c == '"') {
|
||||
return arg;
|
||||
@@ -272,6 +285,7 @@ fn quote_step_arg(arg: String) -> String {
|
||||
format!("\"{escaped}\"")
|
||||
}
|
||||
|
||||
/// Checks if compiler is MSVC based on name.
|
||||
fn is_msvc_compiler(compiler: &str) -> bool {
|
||||
let lower = compiler.to_lowercase();
|
||||
lower.contains("cl.exe")
|
||||
@@ -280,6 +294,7 @@ fn is_msvc_compiler(compiler: &str) -> bool {
|
||||
|| lower.contains("msvc")
|
||||
}
|
||||
|
||||
/// Runs shell command with output capture and cancellation support.
|
||||
fn run_shell_command(
|
||||
cmd: &str,
|
||||
cancel: &Arc<AtomicBool>,
|
||||
@@ -352,6 +367,7 @@ fn run_shell_command(
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
/// Checks if any build step needs to be run by comparing timestamps.
|
||||
pub(crate) fn build_steps_need_run(steps: &[BuildStep], vars: &StepVars) -> Result<bool, String> {
|
||||
for step in steps {
|
||||
let input_pattern = expand_step_value(&step.input, "", vars);
|
||||
@@ -380,6 +396,7 @@ pub(crate) fn build_steps_need_run(steps: &[BuildStep], vars: &StepVars) -> Resu
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Creates VersionInfo from StepVars.
|
||||
fn make_version_info(vars: &StepVars) -> VersionInfo {
|
||||
VersionInfo {
|
||||
full: vars.version.to_string(),
|
||||
@@ -391,6 +408,7 @@ fn make_version_info(vars: &StepVars) -> VersionInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Expands step value by substituting vars and stem.
|
||||
fn expand_step_value(template: &str, stem: &str, vars: &StepVars) -> String {
|
||||
let info = make_version_info(vars);
|
||||
let s = substitute_vars(template, &info, vars.profile, "");
|
||||
|
||||
@@ -15,12 +15,13 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use serde::Deserialize;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use toml::Value;
|
||||
use toml::map::Map;
|
||||
use toml::{Value, map::Map};
|
||||
use toml_edit::{DocumentMut, Item, Table};
|
||||
|
||||
const DEFAULT_VERSION: &str = "0.1.0";
|
||||
@@ -39,6 +40,7 @@ const VALID_KINDS: &[&str] = &[
|
||||
"flat-bin",
|
||||
];
|
||||
|
||||
/// Represents errors that can occur during configuration loading, parsing, or validation.
|
||||
#[derive(Debug)]
|
||||
pub enum ConfigError {
|
||||
Io(std::io::Error),
|
||||
@@ -80,6 +82,7 @@ impl From<toml::ser::Error> for ConfigError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Core configuration structure that manages TOML parsing, typed views, and validation for DCR projects.
|
||||
pub struct Config {
|
||||
path: PathBuf,
|
||||
data: Value,
|
||||
@@ -91,6 +94,7 @@ pub struct Config {
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Top-level typed configuration parsed from TOML, containing optional sections for package, build, dependencies, etc.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct DcrConfig {
|
||||
#[serde(default)]
|
||||
@@ -108,6 +112,7 @@ pub struct DcrConfig {
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Configuration for archive output formats (e.g. EFI, bootable images).
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ArchiveConfig {
|
||||
pub output: String,
|
||||
@@ -121,16 +126,24 @@ pub struct ArchiveConfig {
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Layout definition for archive sections (from/to paths).
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ArchiveLayout {
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
}
|
||||
|
||||
/// Package metadata section (name, version, type, dcr-version).
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct PackageConfig {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
/// Minimum/target DCR tool version this project was authored for (`package.dcr-version`).
|
||||
///
|
||||
/// Compared against the running `dcr` binary (`CARGO_PKG_VERSION`) on config load.
|
||||
/// Missing or empty means "no pin" — no warning is emitted.
|
||||
#[serde(default, rename = "dcr-version")]
|
||||
pub dcr_version: Option<String>,
|
||||
#[serde(default, rename = "type")]
|
||||
pub pkg_type: Option<String>,
|
||||
}
|
||||
@@ -144,6 +157,7 @@ fn default_compiler() -> String {
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Build configuration section, including language, standards, compiler, and output settings.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct BuildConfig {
|
||||
#[serde(default = "default_language")]
|
||||
@@ -182,6 +196,7 @@ pub struct BuildConfig {
|
||||
pub qt: Option<bool>,
|
||||
}
|
||||
|
||||
/// Language configuration can be a single string or array of strings (e.g. for multi-language builds).
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum LanguageConfig {
|
||||
@@ -198,6 +213,7 @@ impl LanguageConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Dependency configuration can be a version string or a full table of dependency details.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
@@ -206,6 +222,7 @@ pub enum DependencyConfig {
|
||||
Table(BTreeMap<String, Value>),
|
||||
}
|
||||
|
||||
/// Toolchain configuration for custom compilers, assemblers, and linkers.
|
||||
#[derive(Debug, Clone, Deserialize, Default)]
|
||||
pub struct ToolchainConfig {
|
||||
pub cc: Option<String>,
|
||||
@@ -216,6 +233,7 @@ pub struct ToolchainConfig {
|
||||
pub ld: Option<String>,
|
||||
}
|
||||
|
||||
/// Workspace member configuration for multi-project workspaces.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct WorkspaceMemberConfig {
|
||||
@@ -227,12 +245,19 @@ pub struct WorkspaceMemberConfig {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
#[allow(dead_code)]
|
||||
/// Creates a new Config instance from a TOML file path. If the file does not exist, it creates a default one.
|
||||
pub fn new(path: &str) -> Result<Self, ConfigError> {
|
||||
Self::create(path, None)
|
||||
}
|
||||
|
||||
/// Creates a new Config instance with an explicit package name for the default template when creating a new file.
|
||||
pub fn create(path: &str, package_name: Option<&str>) -> Result<Self, ConfigError> {
|
||||
let path = PathBuf::from(path);
|
||||
let (data, doc) = if path.exists() {
|
||||
load_parts(&path)?
|
||||
} else {
|
||||
let content = default_toml_text();
|
||||
let content = default_toml_text(package_name)?;
|
||||
let doc: DocumentMut = content.parse().map_err(ConfigError::TomlEdit)?;
|
||||
fs::write(&path, doc.to_string())?;
|
||||
let data: Value = toml::from_str(&content)?;
|
||||
@@ -246,9 +271,11 @@ impl Config {
|
||||
doc,
|
||||
};
|
||||
cfg.validate()?;
|
||||
cfg.warn_dcr_version_mismatch();
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
/// Opens an existing Config from a TOML file path, failing if the file does not exist.
|
||||
pub fn open(path: &str) -> Result<Self, ConfigError> {
|
||||
let path = PathBuf::from(path);
|
||||
if !path.exists() {
|
||||
@@ -263,20 +290,86 @@ impl Config {
|
||||
doc,
|
||||
};
|
||||
cfg.validate()?;
|
||||
cfg.warn_dcr_version_mismatch();
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Returns the fully typed configuration struct.
|
||||
pub fn typed(&self) -> &DcrConfig {
|
||||
&self.typed
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Returns the package configuration if present.
|
||||
pub fn package(&self) -> Option<&PackageConfig> {
|
||||
self.typed.package.as_ref()
|
||||
}
|
||||
|
||||
/// Warn once per config path about `package.dcr-version` vs this `dcr` binary.
|
||||
///
|
||||
/// - Missing/`""` pin → recommend adding `dcr-version`.
|
||||
/// - Tool older than pin → project may use features this binary does not support.
|
||||
/// - Tool newer than pin → project may rely on outdated defaults; consider bumping the pin.
|
||||
///
|
||||
/// Never fails the load; warnings only. Equal pin is silent.
|
||||
fn warn_dcr_version_mismatch(&self) {
|
||||
let Some(pkg) = self.typed.package.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let path_key = self
|
||||
.path
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| self.path.clone())
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
if !dcr_version_warn_once(&path_key) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tool = env!("CARGO_PKG_VERSION");
|
||||
let required = pkg
|
||||
.dcr_version
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty());
|
||||
let Some(required) = required else {
|
||||
warn!(
|
||||
"package.dcr-version is missing in {}. Add e.g. dcr-version = \"{tool}\" \
|
||||
so collaborators know which dcr this project targets \
|
||||
(written automatically by `dcr new` / `dcr init`).",
|
||||
self.path.display(),
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
match crate::utils::build::compare_semver(tool, required) {
|
||||
Some(std::cmp::Ordering::Less) => {
|
||||
warn!(
|
||||
"this project requires dcr {required} (package.dcr-version), \
|
||||
but you are running dcr {tool}. Some features may be missing or misbehave. \
|
||||
Upgrade dcr (e.g. `dcr --update`) or lower package.dcr-version if intentional."
|
||||
);
|
||||
}
|
||||
Some(std::cmp::Ordering::Greater) => {
|
||||
warn!(
|
||||
"this project pins dcr {required} (package.dcr-version), \
|
||||
but you are running newer dcr {tool}. Config/defaults may be outdated. \
|
||||
Review the changelog and bump package.dcr-version when the project is verified.",
|
||||
);
|
||||
}
|
||||
Some(std::cmp::Ordering::Equal) => {}
|
||||
None => {
|
||||
warn!(
|
||||
"package.dcr-version = \"{required}\" is not a valid semver (expected X.Y.Z); ignoring",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Returns the build configuration if present.
|
||||
pub fn build_config(&self) -> Option<&BuildConfig> {
|
||||
self.typed.build.as_ref()
|
||||
}
|
||||
@@ -290,7 +383,7 @@ impl Config {
|
||||
&& !self.typed.workspace.is_empty()
|
||||
}
|
||||
|
||||
/// Merge build fields from parent config into self where self has empty/default values.
|
||||
/// Merges build fields from a parent config into self where self has empty/default values.
|
||||
/// Used for workspace member inheritance (build.inherit = true).
|
||||
pub fn merge_parent(&mut self, parent: &Config) {
|
||||
let fields: &[&str] = &[
|
||||
@@ -368,24 +461,29 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves a nested value from the config data using dot-separated path.
|
||||
pub fn get(&self, key: &str) -> Option<&Value> {
|
||||
let parts: Vec<&str> = key.split('.').collect();
|
||||
get_path(&self.data, &parts)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Adds a new key-value pair to the configuration (alias for set).
|
||||
pub fn add(&mut self, key: &str, value: Value) -> Result<(), ConfigError> {
|
||||
self.set(key, value)
|
||||
}
|
||||
|
||||
/// Updates a configuration value at the given key path.
|
||||
pub fn edit(&mut self, key: &str, value: Value) -> Result<(), ConfigError> {
|
||||
self.set(key, value)
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
/// Checks if the configuration is valid without returning the error.
|
||||
pub fn check(&self) -> bool {
|
||||
self.validate().is_ok()
|
||||
}
|
||||
|
||||
/// Performs comprehensive validation of the configuration, including package, build, toolchain, and workspace rules.
|
||||
pub fn validate(&self) -> Result<(), ConfigError> {
|
||||
if self.is_workspace_only() {
|
||||
return self.validate_workspace();
|
||||
@@ -739,16 +837,24 @@ fn load_parts(path: &Path) -> Result<(Value, DocumentMut), ConfigError> {
|
||||
Ok((data, doc))
|
||||
}
|
||||
|
||||
fn default_toml_text() -> String {
|
||||
let name = std::env::current_dir()
|
||||
.ok()
|
||||
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string()))
|
||||
fn default_toml_text(package_name: Option<&str>) -> Result<String, ConfigError> {
|
||||
let name = package_name
|
||||
.map(|s| s.to_string())
|
||||
.or_else(|| {
|
||||
std::env::current_dir()
|
||||
.ok()
|
||||
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string()))
|
||||
})
|
||||
.unwrap_or_else(|| "project".to_string());
|
||||
validate_non_empty_string(&name, "package.name")?;
|
||||
validate_package_name(&name)?;
|
||||
let dcr_version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
format!(
|
||||
Ok(format!(
|
||||
"[package]\n\
|
||||
name = \"{name}\"\n\
|
||||
version = \"{DEFAULT_VERSION}\"\n\
|
||||
dcr-version = \"{dcr_version}\"\n\
|
||||
type = \"none\"\n\
|
||||
description = \"\"\n\
|
||||
author = \"\"\n\
|
||||
@@ -761,7 +867,18 @@ fn default_toml_text() -> String {
|
||||
kind = \"{DEFAULT_KIND}\"\n\
|
||||
\n\
|
||||
[dependencies]\n"
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
fn dcr_version_warn_once(path_key: &str) -> bool {
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
static WARNED: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
|
||||
let set = WARNED.get_or_init(|| Mutex::new(HashSet::new()));
|
||||
let Ok(mut guard) = set.lock() else {
|
||||
return true;
|
||||
};
|
||||
guard.insert(path_key.to_string())
|
||||
}
|
||||
|
||||
fn set_doc_path(doc: &mut DocumentMut, path: &[&str], value: &Value) -> Result<(), ConfigError> {
|
||||
@@ -916,11 +1033,21 @@ mod tests {
|
||||
let dir = temp_dir("typed_config");
|
||||
let path = write_toml_file(
|
||||
&dir,
|
||||
"[package]\nname = \"typed\"\nversion = \"1.2.3\"\ntype = \"lib\"\n\n[build]\nlanguage = [\"c\", \"c++\"]\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"staticlib\"\ncflags = [\"-Wall\"]\n\n[dependencies]\nfoo = \"1.0.0\"\n",
|
||||
"[package]\nname = \"typed\"\nversion = \"1.2.3\"\ndcr-version = \"0.8.4\"\ntype = \"lib\"\n\n[build]\nlanguage = [\"c\", \"c++\"]\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"staticlib\"\ncflags = [\"-Wall\"]\n\n[dependencies]\nfoo = \"1.0.0\"\n",
|
||||
);
|
||||
let config = Config::open(&path.to_string_lossy()).unwrap();
|
||||
assert_eq!(config.package().unwrap().name, "typed");
|
||||
assert_eq!(config.typed().package.as_ref().unwrap().version, "1.2.3");
|
||||
assert_eq!(
|
||||
config
|
||||
.typed()
|
||||
.package
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.dcr_version
|
||||
.as_deref(),
|
||||
Some("0.8.4")
|
||||
);
|
||||
assert_eq!(config.build_config().unwrap().compiler, "clang");
|
||||
assert_eq!(config.build_config().unwrap().cflags, ["-Wall"]);
|
||||
assert!(config.typed().dependencies.contains_key("foo"));
|
||||
@@ -1171,6 +1298,22 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_rejects_invalid_name_without_writing_manifest() {
|
||||
let dir = temp_dir("invalid_create_name");
|
||||
let path = dir.join("dcr.toml");
|
||||
for name in ["", "bad name", "../bad", "bad\"name", " bad", "bad."] {
|
||||
let result = Config::create(&path.to_string_lossy(), Some(name));
|
||||
assert!(matches!(result, Err(ConfigError::Invalid(_))), "{name:?}");
|
||||
assert!(!path.exists(), "invalid name must not create a manifest");
|
||||
}
|
||||
let config = Config::create(&path.to_string_lossy(), Some("valid-name")).unwrap();
|
||||
assert_eq!(
|
||||
config.get("package.name").and_then(Value::as_str),
|
||||
Some("valid-name")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_creates_default_config() {
|
||||
let dir = temp_dir("new_default");
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
/// Represents resolved dependencies for a build target, including include directories, library directories, and libraries.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ResolvedDeps {
|
||||
pub include_dirs: Vec<String>,
|
||||
pub lib_dirs: Vec<String>,
|
||||
pub libs: Vec<String>,
|
||||
pub cflags: Vec<String>,
|
||||
pub ldflags: Vec<String>,
|
||||
pub package_roots: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::{fs, path::Path};
|
||||
|
||||
/// Represents a locked dependency entry containing its name, version, source, and checksum.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DepLock {
|
||||
pub name: String,
|
||||
@@ -26,6 +26,7 @@ pub struct DepLock {
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
/// Writes the dcr.lock file in a Cargo-like format. The project entry comes first, followed by dependency entries.
|
||||
pub fn write_lock(
|
||||
project_root: &Path,
|
||||
project_name: &str,
|
||||
@@ -47,6 +48,7 @@ pub fn write_lock(
|
||||
}
|
||||
out.push('\n');
|
||||
|
||||
// Write metadata for each package in the dependencies list
|
||||
for pkg in packages {
|
||||
out.push_str("[[package]]\n");
|
||||
out.push_str(&format!("name = \"{}\"\n", escape_value(&pkg.name)));
|
||||
@@ -60,6 +62,7 @@ pub fn write_lock(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Converts a list of strings into a comma-separated TOML array by quoting each item.
|
||||
fn quote_list(items: &[String]) -> String {
|
||||
items
|
||||
.iter()
|
||||
@@ -68,6 +71,7 @@ fn quote_list(items: &[String]) -> String {
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
/// Escapes backslashes and double quotes in a string to make it safe for TOML.
|
||||
fn escape_value(input: &str) -> String {
|
||||
input.replace('\\', "\\\\").replace('"', "\\\"")
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/// Core dependency resolver for DCR projects.
|
||||
pub mod common;
|
||||
pub mod lock;
|
||||
pub mod register;
|
||||
@@ -22,8 +23,10 @@ pub mod register;
|
||||
use crate::core::build_config::Config;
|
||||
use crate::core::deps::common::ResolvedDeps;
|
||||
use crate::core::deps::lock::{DepLock, write_lock};
|
||||
use crate::utils::build::{resolve_pkg_config_flags, run_pkg_config};
|
||||
use std::path::Path;
|
||||
|
||||
/// Returns the version string from a dependency's dcr.toml file, or an empty string if missing.
|
||||
fn dep_version(path: &Path) -> String {
|
||||
Config::open(&path.join("dcr.toml").to_string_lossy())
|
||||
.ok()
|
||||
@@ -35,6 +38,18 @@ fn dep_version(path: &Path) -> String {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Resolves dependencies from config into include/lib dirs and lib names, and writes the lock file.
|
||||
///
|
||||
/// Registry, path, and git deps contribute to `ResolvedDeps`. Path and git sources support both
|
||||
/// DCR packages (built from `dcr.toml`) and prebuilt libraries (described with include/lib/libs).
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `config`: Loaded `dcr.toml`.
|
||||
/// - `_profile` / `_target`: Reserved for future profile/target-specific deps.
|
||||
/// - `project_root`: Root used to resolve path deps and write `dcr.lock`.
|
||||
///
|
||||
/// # Returns
|
||||
/// Aggregated include/lib/libs, or an error string on resolve/lock failure.
|
||||
pub fn resolve_deps(
|
||||
config: &Config,
|
||||
_profile: &str,
|
||||
@@ -57,8 +72,24 @@ pub fn resolve_deps(
|
||||
let mut lock_packages: Vec<DepLock> = Vec::new();
|
||||
|
||||
if let Some(deps) = deps_table {
|
||||
// Process each dependency declaration in the TOML table
|
||||
for (name, value) in deps {
|
||||
if register::is_registry_dep(value) {
|
||||
if let Some(packages) = pkg_config_dep(value, name)? {
|
||||
let (cflags, ldflags) = resolve_pkg_config_flags(&packages, &[], &[])?;
|
||||
resolved.cflags.extend(cflags);
|
||||
resolved.ldflags.extend(ldflags);
|
||||
let version = packages
|
||||
.first()
|
||||
.and_then(|pkg| run_pkg_config(pkg, "--modversion").ok())
|
||||
.map(|v| v.trim().to_string())
|
||||
.unwrap_or_default();
|
||||
lock_packages.push(DepLock {
|
||||
name: name.clone(),
|
||||
version,
|
||||
checksum: String::new(),
|
||||
source: format!("pkg-config+{}", packages.join(",")),
|
||||
});
|
||||
} else if register::is_registry_dep(value) {
|
||||
let pkg_info = register::resolve_package_from_registry(name)?;
|
||||
let dep_root = register::package_root_from_registry_info(&pkg_info)?;
|
||||
|
||||
@@ -73,6 +104,9 @@ pub fn resolve_deps(
|
||||
.to_string(),
|
||||
);
|
||||
resolved.libs.push(name.clone());
|
||||
resolved
|
||||
.package_roots
|
||||
.push(dep_root.to_string_lossy().to_string());
|
||||
|
||||
lock_packages.push(DepLock {
|
||||
name: name.clone(),
|
||||
@@ -92,6 +126,8 @@ pub fn resolve_deps(
|
||||
});
|
||||
} else if let Some(path) = path_dep_path(value) {
|
||||
let dep_root = project_root.join(path);
|
||||
let dcr_package_name = package_name(&dep_root);
|
||||
let use_dcr_package = use_dcr_package(value, dcr_package_name.is_some(), name)?;
|
||||
if let Some(table) = value.as_table() {
|
||||
if let Some(includes) = table.get("include").and_then(|v| v.as_array()) {
|
||||
for inc in includes {
|
||||
@@ -103,10 +139,7 @@ pub fn resolve_deps(
|
||||
}
|
||||
} else {
|
||||
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
|
||||
push_if_exists(
|
||||
&mut resolved.include_dirs,
|
||||
&dep_root.join("target").join("include"),
|
||||
);
|
||||
push_package_output_dirs(&mut resolved, &dep_root);
|
||||
}
|
||||
|
||||
if let Some(lib_dirs) = table.get("lib").and_then(|v| v.as_array()) {
|
||||
@@ -128,16 +161,29 @@ pub fn resolve_deps(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resolved.libs.push(name.clone());
|
||||
resolved
|
||||
.libs
|
||||
.push(dcr_package_name.clone().unwrap_or_else(|| name.clone()));
|
||||
}
|
||||
} else {
|
||||
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
|
||||
push_if_exists(
|
||||
&mut resolved.include_dirs,
|
||||
&dep_root.join("target").join("include"),
|
||||
);
|
||||
push_package_output_dirs(&mut resolved, &dep_root);
|
||||
push_default_lib_dirs(&mut resolved.lib_dirs, &dep_root);
|
||||
resolved.libs.push(name.clone());
|
||||
resolved
|
||||
.libs
|
||||
.push(dcr_package_name.clone().unwrap_or_else(|| name.clone()));
|
||||
}
|
||||
|
||||
if use_dcr_package {
|
||||
resolved
|
||||
.package_roots
|
||||
.push(dep_root.to_string_lossy().to_string());
|
||||
push_package_output_dirs(&mut resolved, &dep_root);
|
||||
} else if !has_complete_prebuilt_layout(value) {
|
||||
return Err(format!(
|
||||
"Path dependency `{}` has no dcr.toml; specify include, lib, and libs for a prebuilt library",
|
||||
name
|
||||
));
|
||||
}
|
||||
|
||||
lock_packages.push(DepLock {
|
||||
@@ -147,16 +193,52 @@ pub fn resolve_deps(
|
||||
source: format!("path+{}", dep_root.display()),
|
||||
});
|
||||
} else if let Some(git_info) = git_dep(value) {
|
||||
let dep_root = register::fetch_git_dependency(
|
||||
git_info.url,
|
||||
git_info.branch.as_deref(),
|
||||
git_info.tag.as_deref(),
|
||||
git_info.rev.as_deref(),
|
||||
)?;
|
||||
let dcr_package_name = package_name(&dep_root);
|
||||
let use_dcr_package = use_dcr_package(value, dcr_package_name.is_some(), name)?;
|
||||
let (has_include, has_lib, has_libs) = value
|
||||
.as_table()
|
||||
.map(|table| push_explicit_layout(&mut resolved, &dep_root, table))
|
||||
.unwrap_or((false, false, false));
|
||||
if !has_include {
|
||||
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
|
||||
if use_dcr_package {
|
||||
push_package_output_dirs(&mut resolved, &dep_root);
|
||||
}
|
||||
}
|
||||
if !has_lib {
|
||||
push_default_lib_dirs(&mut resolved.lib_dirs, &dep_root);
|
||||
}
|
||||
if !has_libs && let Some(package_name) = dcr_package_name.as_deref() {
|
||||
resolved.libs.push(package_name.to_string());
|
||||
}
|
||||
if use_dcr_package {
|
||||
resolved
|
||||
.package_roots
|
||||
.push(dep_root.to_string_lossy().to_string());
|
||||
} else if !(has_include && has_lib && has_libs) {
|
||||
return Err(format!(
|
||||
"Git dependency `{}` has no dcr.toml; specify include, lib, and libs for a prebuilt library",
|
||||
name
|
||||
));
|
||||
}
|
||||
let commit = register::git_commit(&dep_root)?;
|
||||
lock_packages.push(DepLock {
|
||||
name: name.clone(),
|
||||
version: git_info.version.unwrap_or_default(),
|
||||
checksum: String::new(),
|
||||
source: format!("git+{}", git_info.url),
|
||||
version: git_info.version.unwrap_or(commit.clone()),
|
||||
checksum: commit.clone(),
|
||||
source: format!("git+{}#{}", git_info.url, commit),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write the lock file with all resolved dependencies
|
||||
write_lock(
|
||||
project_root,
|
||||
&project_name,
|
||||
@@ -167,11 +249,16 @@ pub fn resolve_deps(
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
/// Git dependency specification (url + optional version/ref for the lockfile).
|
||||
struct GitDep<'a> {
|
||||
url: &'a str,
|
||||
version: Option<String>,
|
||||
branch: Option<String>,
|
||||
tag: Option<String>,
|
||||
rev: Option<String>,
|
||||
}
|
||||
|
||||
/// Parses a git dependency from a TOML value table.
|
||||
fn git_dep(value: &toml::Value) -> Option<GitDep<'_>> {
|
||||
if let Some(table) = value.as_table()
|
||||
&& let Some(url) = table.get("git").and_then(|v| v.as_str())
|
||||
@@ -180,11 +267,72 @@ fn git_dep(value: &toml::Value) -> Option<GitDep<'_>> {
|
||||
.get("version")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
return Some(GitDep { url, version });
|
||||
let branch = table
|
||||
.get("branch")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::to_string);
|
||||
let tag = table
|
||||
.get("tag")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::to_string);
|
||||
let rev = table
|
||||
.get("rev")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::to_string);
|
||||
return Some(GitDep {
|
||||
url,
|
||||
version,
|
||||
branch,
|
||||
tag,
|
||||
rev,
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Extracts one or more pkg-config package names from a dependency table.
|
||||
/// Both `pkg-config` and the legacy-style `pkg_config` spelling are accepted.
|
||||
fn pkg_config_dep(
|
||||
value: &toml::Value,
|
||||
dependency_name: &str,
|
||||
) -> Result<Option<Vec<String>>, String> {
|
||||
let Some(table) = value.as_table() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let raw = table.get("pkg-config").or_else(|| table.get("pkg_config"));
|
||||
let Some(raw) = raw else {
|
||||
return Ok(None);
|
||||
};
|
||||
let packages = if let Some(pkg) = raw.as_str() {
|
||||
vec![pkg.to_string()]
|
||||
} else {
|
||||
raw.as_array()
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"Dependency `{dependency_name}` field `pkg-config` must be a string or array of strings"
|
||||
)
|
||||
})?
|
||||
.iter()
|
||||
.map(|value| {
|
||||
value.as_str().map(str::to_string).ok_or_else(|| {
|
||||
format!(
|
||||
"Dependency `{dependency_name}` field `pkg-config` must contain only strings"
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.collect()
|
||||
};
|
||||
if packages.is_empty() || packages.iter().any(|pkg| pkg.trim().is_empty()) {
|
||||
return Err(format!(
|
||||
"Dependency `{dependency_name}` field `pkg-config` must not be empty"
|
||||
));
|
||||
}
|
||||
Ok(Some(packages))
|
||||
}
|
||||
|
||||
/// Extracts the path for a path-based dependency, supporting both table format and legacy string format.
|
||||
fn path_dep_path(value: &toml::Value) -> Option<&str> {
|
||||
if let Some(table) = value.as_table() {
|
||||
return table.get("path").and_then(|v| v.as_str());
|
||||
@@ -192,12 +340,14 @@ fn path_dep_path(value: &toml::Value) -> Option<&str> {
|
||||
register::path_from_string_dep(value)
|
||||
}
|
||||
|
||||
/// Adds a directory path to the list if it exists on the filesystem.
|
||||
fn push_if_exists(paths: &mut Vec<String>, path: &Path) {
|
||||
if path.exists() {
|
||||
paths.push(path.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds common library directory candidates to the list if they exist.
|
||||
fn push_default_lib_dirs(paths: &mut Vec<String>, dep_root: &Path) {
|
||||
for dir in ["lib", "lib64"] {
|
||||
push_if_exists(paths, &dep_root.join(dir));
|
||||
@@ -205,11 +355,123 @@ fn push_default_lib_dirs(paths: &mut Vec<String>, dep_root: &Path) {
|
||||
push_if_exists(paths, &dep_root.join("target").join("lib"));
|
||||
}
|
||||
|
||||
/// Adds explicitly configured prebuilt include/lib directories and linker names.
|
||||
fn push_explicit_layout(
|
||||
resolved: &mut ResolvedDeps,
|
||||
dep_root: &Path,
|
||||
table: &toml::map::Map<String, toml::Value>,
|
||||
) -> (bool, bool, bool) {
|
||||
let mut has_include = false;
|
||||
let mut has_lib = false;
|
||||
let mut has_libs = false;
|
||||
if let Some(includes) = table.get("include").and_then(|v| v.as_array()) {
|
||||
has_include = true;
|
||||
for inc in includes {
|
||||
if let Some(path) = inc.as_str() {
|
||||
resolved
|
||||
.include_dirs
|
||||
.push(dep_root.join(path).to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(lib_dirs) = table.get("lib").and_then(|v| v.as_array()) {
|
||||
has_lib = true;
|
||||
for path in lib_dirs {
|
||||
if let Some(path) = path.as_str() {
|
||||
resolved
|
||||
.lib_dirs
|
||||
.push(dep_root.join(path).to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(libs) = table.get("libs").and_then(|v| v.as_array()) {
|
||||
has_libs = true;
|
||||
for lib in libs {
|
||||
if let Some(name) = lib.as_str() {
|
||||
resolved.libs.push(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
(has_include, has_lib, has_libs)
|
||||
}
|
||||
|
||||
/// Reports whether a source without a DCR manifest fully describes its prebuilt layout.
|
||||
fn has_complete_prebuilt_layout(value: &toml::Value) -> bool {
|
||||
value.as_table().is_some_and(|table| {
|
||||
["include", "lib", "libs"].iter().all(|field| {
|
||||
table
|
||||
.get(*field)
|
||||
.and_then(|value| value.as_array())
|
||||
.is_some_and(|values| !values.is_empty())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Selects automatic, forced-build, or forced-prebuilt dependency resolution.
|
||||
fn use_dcr_package(
|
||||
value: &toml::Value,
|
||||
has_manifest: bool,
|
||||
dependency_name: &str,
|
||||
) -> Result<bool, String> {
|
||||
let mode = value
|
||||
.as_table()
|
||||
.and_then(|table| table.get("mode"))
|
||||
.map(|value| {
|
||||
value.as_str().ok_or_else(|| {
|
||||
format!(
|
||||
"Dependency `{dependency_name}` field `mode` must be \"build\" or \"prebuild\""
|
||||
)
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
match mode {
|
||||
None => Ok(has_manifest),
|
||||
Some("build") if has_manifest => Ok(true),
|
||||
Some("build") => Err(format!(
|
||||
"Dependency `{dependency_name}` uses mode = \"build\" but has no dcr.toml"
|
||||
)),
|
||||
Some("prebuild") => Ok(false),
|
||||
Some(_) => Err(format!(
|
||||
"Dependency `{dependency_name}` field `mode` must be \"build\" or \"prebuild\""
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds DCR's packaged output directories even before the dependency is built.
|
||||
fn push_package_output_dirs(resolved: &mut ResolvedDeps, dep_root: &Path) {
|
||||
push_unique_path(
|
||||
&mut resolved.include_dirs,
|
||||
dep_root.join("target").join("include"),
|
||||
);
|
||||
push_unique_path(&mut resolved.lib_dirs, dep_root.join("target").join("lib"));
|
||||
}
|
||||
|
||||
/// Adds a path once, preserving the linker and include-search order.
|
||||
fn push_unique_path(paths: &mut Vec<String>, path: std::path::PathBuf) {
|
||||
let path = path.to_string_lossy().to_string();
|
||||
if !paths.contains(&path) {
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads a DCR package name when the dependency has a manifest.
|
||||
fn package_name(path: &Path) -> Option<String> {
|
||||
Config::open(&path.join("dcr.toml").to_string_lossy())
|
||||
.ok()
|
||||
.and_then(|config| {
|
||||
config
|
||||
.get("package.name")
|
||||
.and_then(|value| value.as_str())
|
||||
.map(str::to_string)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use toml::Value;
|
||||
|
||||
/// Tests that path_dep_path correctly handles both TOML table and legacy string inputs for path dependencies.
|
||||
#[test]
|
||||
fn path_dep_path_supports_table_and_legacy_strings() {
|
||||
let table = Value::Table(
|
||||
@@ -232,6 +494,21 @@ mod tests {
|
||||
assert_eq!(path_dep_path(&Value::String("1.2.3".to_string())), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pkg_config_dep_accepts_string_and_array() {
|
||||
let single: Value = toml::from_str("pkg-config = \"fmt\"").unwrap();
|
||||
assert_eq!(
|
||||
pkg_config_dep(&single, "fmt"),
|
||||
Ok(Some(vec!["fmt".to_string()]))
|
||||
);
|
||||
let many: Value = toml::from_str("pkg_config = [\"fmt\", \"zlib\"]").unwrap();
|
||||
assert_eq!(
|
||||
pkg_config_dep(&many, "base"),
|
||||
Ok(Some(vec!["fmt".to_string(), "zlib".to_string()]))
|
||||
);
|
||||
}
|
||||
|
||||
/// Verifies that push_default_lib_dirs includes the target/lib directory when it exists.
|
||||
#[test]
|
||||
fn default_lib_dirs_include_packaged_library_output() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
|
||||
@@ -17,29 +17,36 @@
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
/// Represents a registry configuration entry with its URL and priority.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Registry {
|
||||
pub url: String,
|
||||
pub priority: i32,
|
||||
}
|
||||
|
||||
/// Configuration struct for DCR registries, mapping registry names to Registry details.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct DcrConfig {
|
||||
pub registry: std::collections::HashMap<String, Registry>,
|
||||
}
|
||||
|
||||
/// Helper function to get the user's home directory path.
|
||||
fn home_dir() -> Option<PathBuf> {
|
||||
crate::utils::fs::home_dir()
|
||||
}
|
||||
|
||||
/// Helper to get the DCR config directory path.
|
||||
fn dcr_config_dir() -> Option<PathBuf> {
|
||||
home_dir().map(|h| h.join(".dcr"))
|
||||
}
|
||||
|
||||
/// Loads DCR registry configuration from the TOML file in the user's home directory.
|
||||
pub fn get_registry_config() -> Option<DcrConfig> {
|
||||
let home = home_dir()?;
|
||||
let config_path = home.join(".dcr/config.toml");
|
||||
@@ -51,6 +58,7 @@ pub fn get_registry_config() -> Option<DcrConfig> {
|
||||
toml::from_str(&content).ok()
|
||||
}
|
||||
|
||||
/// Returns the path to the registry index file, preferring the DCR_INDEX_PATH env var or falling back to ~/.dcr/index.json.
|
||||
pub fn get_index_path() -> PathBuf {
|
||||
std::env::var("DCR_INDEX_PATH")
|
||||
.map(PathBuf::from)
|
||||
@@ -61,6 +69,7 @@ pub fn get_index_path() -> PathBuf {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the root directory for registry cache, based on the index path's parent, creating it if necessary.
|
||||
pub fn get_registry_cache_root() -> PathBuf {
|
||||
let index_path = get_index_path();
|
||||
if let Some(parent) = index_path.parent() {
|
||||
@@ -76,6 +85,7 @@ pub fn get_registry_cache_root() -> PathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the package root path from registry info. If the path points to a dcr.toml manifest, returns its parent directory.
|
||||
pub fn package_root_from_registry_info(pkg_info: &JsonValue) -> Result<PathBuf, String> {
|
||||
let raw_path = pkg_info
|
||||
.get("path")
|
||||
@@ -97,6 +107,7 @@ pub fn package_root_from_registry_info(pkg_info: &JsonValue) -> Result<PathBuf,
|
||||
Ok(full_path)
|
||||
}
|
||||
|
||||
/// Converts a raw registry path string to a PathBuf, handling file:// prefixes, Windows extended paths, and relative paths by joining to cache root.
|
||||
pub fn registry_path_to_pathbuf(raw: &str) -> PathBuf {
|
||||
let s = raw.trim();
|
||||
if let Some(rest) = s.strip_prefix("file://") {
|
||||
@@ -130,14 +141,115 @@ pub fn registry_path_to_pathbuf(raw: &str) -> PathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the include directory path for a dependency root, typically under target/include.
|
||||
pub fn registry_include_dir(dep_root: &Path) -> PathBuf {
|
||||
dep_root.join("target").join("include")
|
||||
}
|
||||
|
||||
/// Returns the library directory path for a dependency root, typically under target/lib.
|
||||
pub fn registry_lib_dir(dep_root: &Path) -> PathBuf {
|
||||
dep_root.join("target").join("lib")
|
||||
}
|
||||
|
||||
/// Fetches a git dependency into the DCR cache and returns its working tree.
|
||||
/// The URL and selected ref determine the cache key; an existing checkout is reused.
|
||||
pub fn fetch_git_dependency(
|
||||
url: &str,
|
||||
branch: Option<&str>,
|
||||
tag: Option<&str>,
|
||||
rev: Option<&str>,
|
||||
) -> Result<PathBuf, String> {
|
||||
if !crate::utils::git::is_git_available() {
|
||||
return Err("git executable not found in PATH".to_string());
|
||||
}
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(url.as_bytes());
|
||||
hasher.update(b"\0");
|
||||
hasher.update(branch.unwrap_or("").as_bytes());
|
||||
hasher.update(b"\0");
|
||||
hasher.update(tag.unwrap_or("").as_bytes());
|
||||
hasher.update(b"\0");
|
||||
hasher.update(rev.unwrap_or("").as_bytes());
|
||||
let key = format!("{:x}", hasher.finalize());
|
||||
let root = dcr_config_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join("git")
|
||||
.join(&key);
|
||||
if root.join(".git").is_dir() {
|
||||
let status = Command::new("git")
|
||||
.args(["fetch", "--all", "--tags", "--prune"])
|
||||
.current_dir(&root)
|
||||
.status()
|
||||
.map_err(|e| format!("failed to execute git fetch: {e}"))?;
|
||||
if !status.success() {
|
||||
eprintln!("Warning: git fetch failed for {url}; using the cached checkout");
|
||||
}
|
||||
return checkout_git_ref(&root, branch, tag, rev);
|
||||
}
|
||||
if root.exists() {
|
||||
return Err(format!(
|
||||
"git cache path is not a repository: {}",
|
||||
root.display()
|
||||
));
|
||||
}
|
||||
if let Some(parent) = root.parent() {
|
||||
fs::create_dir_all(parent).map_err(|e| format!("failed to create git cache: {e}"))?;
|
||||
}
|
||||
let status = Command::new("git")
|
||||
.args(["clone", "--", url])
|
||||
.arg(&root)
|
||||
.status()
|
||||
.map_err(|e| format!("failed to execute git clone: {e}"))?;
|
||||
if !status.success() {
|
||||
return Err(format!("git clone failed for {url}"));
|
||||
}
|
||||
checkout_git_ref(&root, branch, tag, rev)
|
||||
}
|
||||
|
||||
/// Returns the exact commit checked out in a cached git dependency.
|
||||
pub fn git_commit(root: &Path) -> Result<String, String> {
|
||||
let output = Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.current_dir(root)
|
||||
.output()
|
||||
.map_err(|e| format!("failed to execute git rev-parse: {e}"))?;
|
||||
if !output.status.success() {
|
||||
return Err(format!("failed to read git revision at {}", root.display()));
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
}
|
||||
|
||||
fn checkout_git_ref(
|
||||
root: &Path,
|
||||
branch: Option<&str>,
|
||||
tag: Option<&str>,
|
||||
rev: Option<&str>,
|
||||
) -> Result<PathBuf, String> {
|
||||
let selected = rev.or(tag).or(branch);
|
||||
let Some(reference) = selected else {
|
||||
return Ok(root.to_path_buf());
|
||||
};
|
||||
let status = Command::new("git")
|
||||
.args(["checkout", "--force", reference])
|
||||
.current_dir(root)
|
||||
.status()
|
||||
.map_err(|e| format!("failed to execute git checkout: {e}"))?;
|
||||
if !status.success() {
|
||||
return Err(format!("git checkout failed for ref `{reference}`"));
|
||||
}
|
||||
Ok(root.to_path_buf())
|
||||
}
|
||||
|
||||
/// Looks up `name` in the local package index (`DCR_INDEX_PATH` / `~/.dcr/index.json`).
|
||||
///
|
||||
/// Configured registries are sorted by priority, but the lookup currently always reads the
|
||||
/// same local index path (per-registry URLs are unused).
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `name`: Package name as in the index.
|
||||
///
|
||||
/// # Returns
|
||||
/// Package JSON object, or an error if config/index is missing or the name is absent.
|
||||
pub fn resolve_package_from_registry(name: &str) -> Result<JsonValue, String> {
|
||||
let config = get_registry_config().ok_or("No registry config found")?;
|
||||
let mut registries: Vec<(&String, &Registry)> = config.registry.iter().collect();
|
||||
@@ -165,6 +277,10 @@ pub fn resolve_package_from_registry(name: &str) -> Result<JsonValue, String> {
|
||||
))
|
||||
}
|
||||
|
||||
/// True if the TOML value is a registry-style dependency.
|
||||
///
|
||||
/// Strings: not path-like and not git-like. Tables: have version/features/optional/registry
|
||||
/// and no `git`/`path`/`url` keys.
|
||||
pub fn is_registry_dep(value: &TomlValue) -> bool {
|
||||
if let Some(raw) = value.as_str() {
|
||||
let raw = raw.trim();
|
||||
@@ -183,6 +299,7 @@ pub fn is_registry_dep(value: &TomlValue) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the path string from a dependency value if it's a path-like dependency.
|
||||
pub fn path_from_string_dep(value: &TomlValue) -> Option<&str> {
|
||||
let raw = value.as_str()?.trim();
|
||||
if let Some(path) = raw.strip_prefix("path:") {
|
||||
@@ -194,6 +311,7 @@ pub fn path_from_string_dep(value: &TomlValue) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Helper to check if a string represents a local file path.
|
||||
fn is_path_like_string(raw: &str) -> bool {
|
||||
raw.starts_with("path:")
|
||||
|| raw.starts_with("./")
|
||||
@@ -203,6 +321,7 @@ fn is_path_like_string(raw: &str) -> bool {
|
||||
|| raw.contains('\\')
|
||||
}
|
||||
|
||||
/// Helper to check if a string represents a git or remote dependency.
|
||||
fn is_git_like_string(raw: &str) -> bool {
|
||||
raw.starts_with("git:")
|
||||
|| raw.starts_with("github:")
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/// Core module for DCR, a Cargo-like C/C++ project manager.
|
||||
pub mod build;
|
||||
pub mod build_config;
|
||||
pub mod deps;
|
||||
|
||||
@@ -15,33 +15,38 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/// Registry configuration management for DCR.
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::{collections::HashMap, fs, path::PathBuf};
|
||||
|
||||
/// Represents a registry configuration entry with its URL and priority.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Registry {
|
||||
pub url: String,
|
||||
pub priority: i32,
|
||||
}
|
||||
|
||||
/// Represents the overall configuration loaded from config.toml, mapping registry names to their details.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub registry: HashMap<String, Registry>,
|
||||
}
|
||||
|
||||
/// Manages the DCR registry configuration, loading from ~/.dcr/config.toml.
|
||||
pub struct RegistryManager {
|
||||
pub config: Config,
|
||||
#[allow(dead_code)]
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
/// Returns the user's home directory path, used to locate the config file.
|
||||
fn home_dir() -> Option<PathBuf> {
|
||||
crate::utils::fs::home_dir()
|
||||
}
|
||||
|
||||
impl RegistryManager {
|
||||
/// Loads the registry configuration from ~/.dcr/config.toml.
|
||||
/// Returns an error if the config file is missing.
|
||||
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let dcr_dir = home_dir()
|
||||
.ok_or("Cannot determine home directory")?
|
||||
@@ -53,6 +58,7 @@ impl RegistryManager {
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&config_path)?;
|
||||
// Deserialize the TOML content into the Config struct
|
||||
let config: Config = toml::from_str(&content)?;
|
||||
|
||||
Ok(Self {
|
||||
|
||||
@@ -16,13 +16,28 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::platform;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::{path::Path, process::Command};
|
||||
|
||||
pub fn run_binary(project_name: &str, profile: &str, target_dir: Option<&str>) -> i32 {
|
||||
/// Runs the built binary for a project, forwarding `bin_args`.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `project_name`: Package/binary name used to resolve the artifact path.
|
||||
/// - `profile`: Build profile segment under `target/`.
|
||||
/// - `target_dir`: Optional artifact directory override.
|
||||
/// - `bin_args`: Arguments passed to the process.
|
||||
///
|
||||
/// # Returns
|
||||
/// Child exit code, or `1` if the binary is missing or cannot be spawned.
|
||||
pub fn run_binary(
|
||||
project_name: &str,
|
||||
profile: &str,
|
||||
target_dir: Option<&str>,
|
||||
bin_args: &[String],
|
||||
) -> i32 {
|
||||
let bin_path = platform::bin_path(profile, project_name, target_dir);
|
||||
// Check if the binary exists before attempting to run it
|
||||
if Path::new(&bin_path).exists() {
|
||||
let status = Command::new(&bin_path).status();
|
||||
let status = Command::new(&bin_path).args(bin_args).status();
|
||||
match status {
|
||||
Ok(s) => {
|
||||
return s.code().unwrap_or(0);
|
||||
@@ -32,5 +47,6 @@ pub fn run_binary(project_name: &str, profile: &str, target_dir: Option<&str>) -
|
||||
}
|
||||
}
|
||||
}
|
||||
// Return 1 if binary not found or execution failed
|
||||
1
|
||||
}
|
||||
|
||||
@@ -15,17 +15,23 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::{fs, path::Path};
|
||||
|
||||
// creating git init and .gitignore
|
||||
/// Initializes a Git repository in the target directory and configures its `.gitignore` file.
|
||||
///
|
||||
/// Executes `git init` via the VCS utility module, creates a default `.gitignore` if none exists,
|
||||
/// or updates an existing `.gitignore` to ensure `.dcr/` and build outputs are ignored.
|
||||
pub fn init(path: &Path) -> Result<(), String> {
|
||||
crate::utils::git::git_init(path)?;
|
||||
|
||||
// Populate default .gitignore entries or append DCR metadata rules.
|
||||
let gitignore_path = path.join(".gitignore");
|
||||
if !gitignore_path.exists() {
|
||||
fs::write(&gitignore_path, "/target\n")
|
||||
fs::write(&gitignore_path, "/target\n.dcr/\n")
|
||||
.map_err(|e| format!("failed to create .gitignore: {}", e))?;
|
||||
} else {
|
||||
crate::utils::fs::ensure_gitignore_has_dcr(path)
|
||||
.map_err(|e| format!("failed to update .gitignore: {}", e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -15,10 +15,12 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/// Module for version control system (VCS) integration.
|
||||
pub mod git;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
/// Represents the version control system kind to use.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum VcsKind {
|
||||
Git,
|
||||
@@ -26,6 +28,7 @@ pub enum VcsKind {
|
||||
}
|
||||
|
||||
impl VcsKind {
|
||||
/// Parses the given string into a VcsKind.
|
||||
pub fn parse(s: &str) -> Result<Self, String> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"git" => Ok(Self::Git),
|
||||
@@ -35,6 +38,7 @@ impl VcsKind {
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the VCS for the provided path.
|
||||
pub fn init_vcs(kind: VcsKind, path: &Path) -> Result<(), String> {
|
||||
match kind {
|
||||
VcsKind::Git => git::init(path),
|
||||
@@ -42,8 +46,9 @@ pub fn init_vcs(kind: VcsKind, path: &Path) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
// look for git repo in parents to avoid nested repos
|
||||
/// Finds an existing VCS in the parent directories of the given path.
|
||||
pub fn find_existing_vcs(path: &Path) -> Option<VcsKind> {
|
||||
// look for git repo in parents to avoid nested repos
|
||||
let mut current = if path.is_file() {
|
||||
path.parent()
|
||||
} else {
|
||||
|
||||
@@ -21,6 +21,7 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
use toml::Value;
|
||||
|
||||
/// Represents a member of the workspace.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WorkspaceMember {
|
||||
pub name: String,
|
||||
@@ -29,12 +30,14 @@ pub struct WorkspaceMember {
|
||||
pub main: bool,
|
||||
}
|
||||
|
||||
/// Represents the workspace as a collection of members.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Workspace {
|
||||
pub members: Vec<WorkspaceMember>,
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
/// Returns the main workspace member, preferring the one with `main: true` or the first member.
|
||||
pub fn main_member(&self) -> Option<&WorkspaceMember> {
|
||||
self.members
|
||||
.iter()
|
||||
@@ -43,6 +46,17 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses the workspace configuration from the TOML config.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `config`: Root `dcr.toml` (may be workspace-only or mixed).
|
||||
/// - `profile` / `target`: Select profile- and target-specific `[workspace.*]` tables.
|
||||
/// - `root`: Workspace root used to resolve member paths.
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Ok(None)` if no workspace table applies.
|
||||
/// - `Ok(Some(Workspace))` with members (and optional main).
|
||||
/// - `Err` on invalid member paths or config shape.
|
||||
pub fn parse_workspace(
|
||||
config: &Config,
|
||||
profile: &str,
|
||||
@@ -50,6 +64,7 @@ pub fn parse_workspace(
|
||||
root: &Path,
|
||||
) -> Result<Option<Workspace>, String> {
|
||||
let mut table = None;
|
||||
// Create ordered list of possible config keys for lookup
|
||||
let combinations = if let Some(t) = target {
|
||||
let normalized_t = normalize_target_os(t);
|
||||
vec![
|
||||
@@ -103,6 +118,7 @@ pub fn parse_workspace(
|
||||
Ok(Some(ws))
|
||||
}
|
||||
|
||||
/// Resolves a path, making it absolute if relative to root.
|
||||
fn resolve_path(root: &Path, raw: &str) -> Result<PathBuf, String> {
|
||||
let p = Path::new(raw);
|
||||
let full = if p.is_absolute() {
|
||||
@@ -113,6 +129,7 @@ fn resolve_path(root: &Path, raw: &str) -> Result<PathBuf, String> {
|
||||
Ok(full)
|
||||
}
|
||||
|
||||
/// Parses dependencies list from TOML value into Vec<String>.
|
||||
fn parse_deps(value: Option<&Value>) -> Result<Vec<String>, String> {
|
||||
let Some(value) = value else {
|
||||
return Ok(Vec::new());
|
||||
@@ -130,12 +147,14 @@ fn parse_deps(value: Option<&Value>) -> Result<Vec<String>, String> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Performs a topological sort on members to handle dependencies and detect cycles.
|
||||
fn topo_sort(members: &[WorkspaceMember]) -> Result<Vec<WorkspaceMember>, String> {
|
||||
let mut map = HashMap::new();
|
||||
for m in members {
|
||||
map.insert(m.name.clone(), m.clone());
|
||||
}
|
||||
let mut state: HashMap<String, u8> = HashMap::new();
|
||||
// Use 0=unvisited, 1=visiting, 2=visited for cycle detection
|
||||
let mut order: Vec<WorkspaceMember> = Vec::new();
|
||||
|
||||
for name in map.keys() {
|
||||
@@ -146,12 +165,14 @@ fn topo_sort(members: &[WorkspaceMember]) -> Result<Vec<WorkspaceMember>, String
|
||||
Ok(order)
|
||||
}
|
||||
|
||||
/// Recursive function for DFS-based topological sort with cycle detection.
|
||||
fn visit(
|
||||
name: &str,
|
||||
map: &HashMap<String, WorkspaceMember>,
|
||||
state: &mut HashMap<String, u8>,
|
||||
order: &mut Vec<WorkspaceMember>,
|
||||
) -> Result<(), String> {
|
||||
// Check current visit state to detect cycles
|
||||
match state.get(name).copied().unwrap_or(0) {
|
||||
1 => return Err(format!("workspace dependency cycle at {name}")),
|
||||
2 => return Ok(()),
|
||||
|
||||
13
src/main.rs
13
src/main.rs
@@ -14,14 +14,23 @@
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
use crate::utils::log::error;
|
||||
mod cli;
|
||||
mod config;
|
||||
mod core;
|
||||
mod platform;
|
||||
mod prelude;
|
||||
mod utils;
|
||||
|
||||
use prelude::*;
|
||||
|
||||
/// Main entry point for DCR, a Cargo-like C/C++ project manager.
|
||||
///
|
||||
/// Collects command-line arguments via `std::env::args()` and dispatches to the
|
||||
/// appropriate subcommand handler. With fewer than two arguments, shows help and
|
||||
/// exits with code 0; otherwise exits with the handler's exit code.
|
||||
fn main() {
|
||||
utils::log::init();
|
||||
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
if args.len() < 2 {
|
||||
@@ -56,7 +65,7 @@ fn main() {
|
||||
"--help" => cli::help::help(),
|
||||
"--update" => cli::flag_update::flag_update(rest),
|
||||
_ => {
|
||||
error("Unknown command or argument");
|
||||
error!("Unknown command or argument");
|
||||
1
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,63 +15,51 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use super::path_util::{default_bin_rel, default_lib_rel, join_dir};
|
||||
use crate::utils::build::default_target_triple;
|
||||
|
||||
/// Returns the host target triple used for default artifact paths on BSD.
|
||||
fn host_triple() -> String {
|
||||
default_target_triple()
|
||||
}
|
||||
|
||||
/// Returns the path to a binary for the given profile, name, and optional target directory.
|
||||
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
|
||||
None => {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let os = std::env::consts::OS;
|
||||
let target = format!("{arch}-unknown-{os}");
|
||||
format!("./target/{target}/{profile}/{name}")
|
||||
}
|
||||
Some(dir) => join_dir(dir, name),
|
||||
None => default_bin_rel(profile, name, Some(&host_triple())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path to a static library for the given profile, name, and optional target directory.
|
||||
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
let file = format!("lib{name}.a");
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
|
||||
None => {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let os = std::env::consts::OS;
|
||||
let target = format!("{arch}-unknown-{os}");
|
||||
format!("./target/{target}/{profile}/lib{name}.a")
|
||||
}
|
||||
Some(dir) => join_dir(dir, &file),
|
||||
None => default_lib_rel(profile, &file, Some(&host_triple())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path to an ELF binary for the given profile, name, and optional target directory.
|
||||
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
|
||||
None => {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let os = std::env::consts::OS;
|
||||
let target = format!("{arch}-unknown-{os}");
|
||||
format!("./target/{target}/{profile}/{name}")
|
||||
}
|
||||
}
|
||||
bin_path(profile, name, target_dir)
|
||||
}
|
||||
|
||||
/// Returns the path to an EFI binary for the given profile, name, and optional target directory.
|
||||
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
let file = format!("{name}.efi");
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
|
||||
None => {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let os = std::env::consts::OS;
|
||||
let target = format!("{arch}-unknown-{os}");
|
||||
format!("./target/{target}/{profile}/{name}.efi")
|
||||
}
|
||||
Some(dir) => join_dir(dir, &file),
|
||||
None => default_lib_rel(profile, &file, Some(&host_triple())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path to a shared library for the given profile, name, and optional target directory.
|
||||
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
let file = format!("lib{name}.so");
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/lib{}.so", dir.trim_end_matches('/'), name),
|
||||
None => {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let os = std::env::consts::OS;
|
||||
let target = format!("{arch}-unknown-{os}");
|
||||
format!("./target/{target}/{profile}/lib{name}.so")
|
||||
}
|
||||
Some(dir) => join_dir(dir, &file),
|
||||
None => default_lib_rel(profile, &file, Some(&host_triple())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,25 +69,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn bin_path_default() {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let os = std::env::consts::OS;
|
||||
let expected = format!("./target/{arch}-unknown-{os}/debug/hello");
|
||||
let expected = default_bin_rel("debug", "hello", Some(&host_triple()));
|
||||
assert_eq!(bin_path("debug", "hello", None), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lib_path_default() {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let os = std::env::consts::OS;
|
||||
let expected = format!("./target/{arch}-unknown-{os}/debug/libmylib.a");
|
||||
let expected = default_lib_rel("debug", "libmylib.a", Some(&host_triple()));
|
||||
assert_eq!(lib_path("debug", "mylib", None), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_lib_path_default() {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let os = std::env::consts::OS;
|
||||
let expected = format!("./target/{arch}-unknown-{os}/debug/libmylib.so");
|
||||
let expected = default_lib_rel("debug", "libmylib.so", Some(&host_triple()));
|
||||
assert_eq!(shared_lib_path("debug", "mylib", None), expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,58 +15,52 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use super::path_util::{default_bin_rel, default_lib_rel, join_dir};
|
||||
use crate::utils::build::default_target_triple;
|
||||
|
||||
/// Returns the default target triple string for the host platform.
|
||||
fn host_triple() -> String {
|
||||
default_target_triple()
|
||||
}
|
||||
|
||||
/// Constructs the path to a binary executable.
|
||||
/// If `target_dir` is provided, joins it with the name; otherwise uses default_bin_rel with the host triple.
|
||||
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
|
||||
None => {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let target = format!("{arch}-unknown-linux-gnu");
|
||||
format!("./target/{target}/{profile}/{name}")
|
||||
}
|
||||
Some(dir) => join_dir(dir, name),
|
||||
None => default_bin_rel(profile, name, Some(&host_triple())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs the path to a static library archive.
|
||||
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
let file = format!("lib{name}.a");
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
|
||||
None => {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let target = format!("{arch}-unknown-linux-gnu");
|
||||
format!("./target/{target}/{profile}/lib{name}.a")
|
||||
}
|
||||
Some(dir) => join_dir(dir, &file),
|
||||
None => default_lib_rel(profile, &file, Some(&host_triple())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs the path to an ELF binary.
|
||||
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
|
||||
None => {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let target = format!("{arch}-unknown-linux-gnu");
|
||||
format!("./target/{target}/{profile}/{name}")
|
||||
}
|
||||
}
|
||||
bin_path(profile, name, target_dir)
|
||||
}
|
||||
|
||||
/// Constructs the path to an EFI binary.
|
||||
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
let file = format!("{name}.efi");
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
|
||||
None => {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let target = format!("{arch}-unknown-linux-gnu");
|
||||
format!("./target/{target}/{profile}/{name}.efi")
|
||||
}
|
||||
Some(dir) => join_dir(dir, &file),
|
||||
None => default_lib_rel(profile, &file, Some(&host_triple())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs the path to a shared library.
|
||||
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
let file = format!("lib{name}.so");
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/lib{}.so", dir.trim_end_matches('/'), name),
|
||||
None => {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let target = format!("{arch}-unknown-linux-gnu");
|
||||
format!("./target/{target}/{profile}/lib{name}.so")
|
||||
}
|
||||
Some(dir) => join_dir(dir, &file),
|
||||
None => default_lib_rel(profile, &file, Some(&host_triple())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,44 +70,49 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn bin_path_default() {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let expected = format!("./target/{arch}-unknown-linux-gnu/debug/hello");
|
||||
let expected = default_bin_rel("debug", "hello", Some(&host_triple()));
|
||||
assert_eq!(bin_path("debug", "hello", None), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bin_path_release() {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let expected = format!("./target/{arch}-unknown-linux-gnu/release/hello");
|
||||
let expected = default_bin_rel("release", "hello", Some(&host_triple()));
|
||||
assert_eq!(bin_path("release", "hello", None), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bin_path_custom_target() {
|
||||
assert_eq!(bin_path("debug", "hello", Some("out")), "out/hello");
|
||||
assert_eq!(
|
||||
bin_path("debug", "hello", Some("out")),
|
||||
join_dir("out", "hello")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bin_path_custom_target_trailing_slash() {
|
||||
assert_eq!(bin_path("debug", "hello", Some("out/")), "out/hello");
|
||||
assert_eq!(
|
||||
bin_path("debug", "hello", Some("out/")),
|
||||
join_dir("out/", "hello")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lib_path_default() {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let expected = format!("./target/{arch}-unknown-linux-gnu/debug/libmylib.a");
|
||||
let expected = default_lib_rel("debug", "libmylib.a", Some(&host_triple()));
|
||||
assert_eq!(lib_path("debug", "mylib", None), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lib_path_custom_target() {
|
||||
assert_eq!(lib_path("debug", "mylib", Some("out")), "out/libmylib.a");
|
||||
assert_eq!(
|
||||
lib_path("debug", "mylib", Some("out")),
|
||||
join_dir("out", "libmylib.a")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_lib_path_default() {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let expected = format!("./target/{arch}-unknown-linux-gnu/debug/libmylib.so");
|
||||
let expected = default_lib_rel("debug", "libmylib.so", Some(&host_triple()));
|
||||
assert_eq!(shared_lib_path("debug", "mylib", None), expected);
|
||||
}
|
||||
|
||||
@@ -121,7 +120,7 @@ mod tests {
|
||||
fn shared_lib_path_custom_target() {
|
||||
assert_eq!(
|
||||
shared_lib_path("release", "mylib", Some("dist")),
|
||||
"dist/libmylib.so"
|
||||
join_dir("dist", "libmylib.so")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,37 +15,48 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/// macOS-specific utilities for constructing paths to build artifacts.
|
||||
///
|
||||
/// This module defines functions to generate paths for binaries, static
|
||||
/// libraries, shared libraries, ELF files, and EFI executables on macOS.
|
||||
use super::path_util::{default_bin_rel, default_lib_rel, join_dir};
|
||||
|
||||
/// Constructs the path to a binary for the given profile, name, and optional target directory.
|
||||
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
|
||||
None => format!("./target/{profile}/{name}"),
|
||||
Some(dir) => join_dir(dir, name),
|
||||
None => default_bin_rel(profile, name, None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs the path to a static library archive for the given profile, name, and optional target directory.
|
||||
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
let file = format!("lib{name}.a");
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
|
||||
None => format!("./target/{profile}/lib{name}.a"),
|
||||
Some(dir) => join_dir(dir, &file),
|
||||
None => default_lib_rel(profile, &file, None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs the path to an ELF binary for the given profile, name, and optional target directory.
|
||||
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
|
||||
None => format!("./target/{profile}/{name}"),
|
||||
}
|
||||
bin_path(profile, name, target_dir)
|
||||
}
|
||||
|
||||
/// Constructs the path to an EFI executable for the given profile, name, and optional target directory.
|
||||
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
let file = format!("{name}.efi");
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
|
||||
None => format!("./target/{profile}/{name}.efi"),
|
||||
Some(dir) => join_dir(dir, &file),
|
||||
None => default_lib_rel(profile, &file, None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs the path to a shared library for the given profile, name, and optional target directory.
|
||||
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
let file = format!("lib{name}.dylib");
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/lib{}.dylib", dir.trim_end_matches('/'), name),
|
||||
None => format!("./target/{profile}/lib{name}.dylib"),
|
||||
Some(dir) => join_dir(dir, &file),
|
||||
None => default_lib_rel(profile, &file, None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ pub mod bsd;
|
||||
pub mod linux;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
mod path_util;
|
||||
#[cfg(not(any(
|
||||
target_os = "linux",
|
||||
target_os = "macos",
|
||||
@@ -39,6 +40,17 @@ pub mod posix;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod windows;
|
||||
|
||||
/// Host OS path for a binary artifact.
|
||||
///
|
||||
/// Backend is chosen at compile time via `cfg!(target_os = …)`, not by the build target triple.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `profile`: Build profile directory segment.
|
||||
/// - `name`: Binary basename (may already include extension on some hosts).
|
||||
/// - `target_dir`: Optional absolute/relative artifact directory override.
|
||||
///
|
||||
/// # Returns
|
||||
/// Path string suitable for the current host layout.
|
||||
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
@@ -75,6 +87,9 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Host OS path for an ELF-style artifact (`profile`, `name`, optional `target_dir`).
|
||||
///
|
||||
/// Backend is chosen at compile time via `cfg!(target_os = …)`, not by the build target triple.
|
||||
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
@@ -111,6 +126,9 @@ pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Host OS path for an EFI (`.efi`) artifact (`profile`, `name`, optional `target_dir`).
|
||||
///
|
||||
/// Backend is chosen at compile time via `cfg!(target_os = …)`, not by the build target triple.
|
||||
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
@@ -147,6 +165,9 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Host OS path for a static library (`profile`, `name`, optional `target_dir`).
|
||||
///
|
||||
/// Backend is chosen at compile time via `cfg!(target_os = …)`, not by the build target triple.
|
||||
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
@@ -183,6 +204,9 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Host OS path for a shared library (`profile`, `name`, optional `target_dir`).
|
||||
///
|
||||
/// Backend is chosen at compile time via `cfg!(target_os = …)`, not by the build target triple.
|
||||
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
|
||||
87
src/platform/path_util.rs
Normal file
87
src/platform/path_util.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
// DCR — Cargo-like C/C++ project manager.
|
||||
//
|
||||
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Joins the given directory and file into a single path string, trimming trailing slashes
|
||||
/// for cross-platform compatibility.
|
||||
pub fn join_dir(dir: &str, file: &str) -> String {
|
||||
let trimmed = dir.trim_end_matches(['/', '\\']);
|
||||
Path::new(trimmed).join(file).to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
#[cfg_attr(not(windows), allow(dead_code))]
|
||||
/// Adds `.exe` suffix to the name if it does not already have a recognized executable
|
||||
/// extension (`.exe`, `.dll`, `.sys`, `.com`, `.efi`) or any extension at all.
|
||||
pub fn with_exe_suffix(name: &str) -> String {
|
||||
let lower = name.to_ascii_lowercase();
|
||||
// Check if name already qualifies for no suffix (has executable ext or any ext)
|
||||
if lower.ends_with(".exe")
|
||||
|| lower.ends_with(".dll")
|
||||
|| lower.ends_with(".sys")
|
||||
|| lower.ends_with(".com")
|
||||
|| lower.ends_with(".efi")
|
||||
|| Path::new(name).extension().is_some()
|
||||
{
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("{name}.exe")
|
||||
}
|
||||
}
|
||||
|
||||
/// Default relative binary path: `./target[/<triple>]/<profile>/<name>`.
|
||||
pub fn default_bin_rel(profile: &str, name: &str, triple_segment: Option<&str>) -> String {
|
||||
let mut p = PathBuf::from("./target");
|
||||
if let Some(t) = triple_segment {
|
||||
p = p.join(t);
|
||||
}
|
||||
p.join(profile).join(name).to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
/// Default relative library path: `./target[/<triple>]/<profile>/<file>`.
|
||||
pub fn default_lib_rel(profile: &str, file: &str, triple_segment: Option<&str>) -> String {
|
||||
let mut p = PathBuf::from("./target");
|
||||
if let Some(t) = triple_segment {
|
||||
p = p.join(t);
|
||||
}
|
||||
p.join(profile).join(file).to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn join_dir_trims_both_slashes() {
|
||||
assert_eq!(
|
||||
join_dir("out/", "a"),
|
||||
Path::new("out").join("a").to_string_lossy()
|
||||
);
|
||||
assert_eq!(
|
||||
join_dir(r"out\", "a"),
|
||||
Path::new("out").join("a").to_string_lossy()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_exe_no_double() {
|
||||
assert_eq!(with_exe_suffix("hello"), "hello.exe");
|
||||
assert_eq!(with_exe_suffix("hello.exe"), "hello.exe");
|
||||
assert_eq!(with_exe_suffix("KERNEL.ELF"), "KERNEL.ELF");
|
||||
assert_eq!(with_exe_suffix("x.dll"), "x.dll");
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,48 @@
|
||||
/// POSIX platform-specific path utilities.
|
||||
/// Provides functions to build paths for binaries, libraries, and EFI files based on profile and optional target directory.
|
||||
use super::path_util::{default_bin_rel, default_lib_rel, join_dir};
|
||||
|
||||
/// Returns the path to a binary executable for the given profile, name, and optional target directory.
|
||||
/// If a target directory is provided, it joins the directory with the name; otherwise, it uses the default binary relative path.
|
||||
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
|
||||
None => format!("./target/{profile}/{name}"),
|
||||
Some(dir) => join_dir(dir, name),
|
||||
None => default_bin_rel(profile, name, None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path to a static library for the given profile, name, and optional target directory.
|
||||
/// The library file is named "lib{name}.a". If a target directory is provided, it joins the directory with the file name; otherwise, it uses the default library relative path.
|
||||
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
let file = format!("lib{name}.a");
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
|
||||
None => format!("./target/{profile}/lib{name}.a"),
|
||||
Some(dir) => join_dir(dir, &file),
|
||||
None => default_lib_rel(profile, &file, None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path to an ELF binary for the given profile, name, and optional target directory.
|
||||
/// Delegates to bin_path since ELF binaries are standard binaries.
|
||||
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
bin_path(profile, name, target_dir)
|
||||
}
|
||||
|
||||
/// Returns the path to an EFI executable for the given profile, name, and optional target directory.
|
||||
/// The EFI file is named "{name}.efi". If a target directory is provided, it joins the directory with the file name; otherwise, it uses the default library relative path.
|
||||
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
let file = format!("{name}.efi");
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
|
||||
None => format!("./target/{profile}/{name}.efi"),
|
||||
Some(dir) => join_dir(dir, &file),
|
||||
None => default_lib_rel(profile, &file, None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path to a shared library for the given profile, name, and optional target directory.
|
||||
/// The shared library file is named "lib{name}.so". If a target directory is provided, it joins the directory with the file name; otherwise, it uses the default library relative path.
|
||||
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
let file = format!("lib{name}.so");
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/lib{}.so", dir.trim_end_matches('/'), name),
|
||||
None => format!("./target/{profile}/lib{name}.so"),
|
||||
Some(dir) => join_dir(dir, &file),
|
||||
None => default_lib_rel(profile, &file, None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,37 +15,63 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use super::path_util::{default_bin_rel, default_lib_rel, join_dir, with_exe_suffix};
|
||||
|
||||
/// Returns the path to the binary executable for the given profile, name, and optional target directory.
|
||||
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
let file = with_exe_suffix(name);
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/{}.exe", dir.trim_end_matches('/'), name),
|
||||
None => format!("./target/{profile}/{name}.exe"),
|
||||
Some(dir) => join_dir(dir, &file),
|
||||
None => default_bin_rel(profile, &file, None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path to the library file for the given profile, name, and optional target directory.
|
||||
/// The library name is forced to end with ".lib".
|
||||
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
// Ensure the library file name ends with ".lib"
|
||||
let file = if name.to_ascii_lowercase().ends_with(".lib") {
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("{name}.lib")
|
||||
};
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/{}.lib", dir.trim_end_matches('/'), name),
|
||||
None => format!("./target/{profile}/{name}.lib"),
|
||||
Some(dir) => join_dir(dir, &file),
|
||||
None => default_lib_rel(profile, &file, None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path to the ELF binary for the given profile, name, and optional target directory.
|
||||
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/{}.exe", dir.trim_end_matches('/'), name),
|
||||
None => format!("./target/{profile}/{name}.exe"),
|
||||
}
|
||||
bin_path(profile, name, target_dir)
|
||||
}
|
||||
|
||||
/// Returns the path to the EFI binary for the given profile, name, and optional target directory.
|
||||
/// The binary name is forced to end with ".efi".
|
||||
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
// Ensure the EFI file name ends with ".efi"
|
||||
let file = if name.to_ascii_lowercase().ends_with(".efi") {
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("{name}.efi")
|
||||
};
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
|
||||
None => format!("./target/{profile}/{name}.efi"),
|
||||
Some(dir) => join_dir(dir, &file),
|
||||
None => default_lib_rel(profile, &file, None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path to the shared library (DLL) for the given profile, name, and optional target directory.
|
||||
/// The library name is forced to end with ".dll".
|
||||
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||
// Ensure the shared library file name ends with ".dll"
|
||||
let file = if name.to_ascii_lowercase().ends_with(".dll") {
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("{name}.dll")
|
||||
};
|
||||
match target_dir {
|
||||
Some(dir) => format!("{}/{}.dll", dir.trim_end_matches('/'), name),
|
||||
None => format!("./target/{profile}/{name}.dll"),
|
||||
Some(dir) => join_dir(dir, &file),
|
||||
None => default_lib_rel(profile, &file, None),
|
||||
}
|
||||
}
|
||||
|
||||
1
src/prelude.rs
Normal file
1
src/prelude.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub use log::{error, warn};
|
||||
@@ -15,9 +15,14 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::build_config::Config;
|
||||
use crate::utils::log::warn;
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::core::build_config::Config;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Version information parsed from a version string.
|
||||
///
|
||||
/// Contains the full version, major, minor, patch, suffix, and suffix_dash components.
|
||||
pub struct VersionInfo {
|
||||
pub full: String,
|
||||
pub major: String,
|
||||
@@ -27,6 +32,40 @@ pub struct VersionInfo {
|
||||
pub suffix_dash: String,
|
||||
}
|
||||
|
||||
/// Compares two semver-like strings (`X.Y.Z` with optional `-suffix`, suffix ignored).
|
||||
///
|
||||
/// Returns `None` if either side has no parseable numeric major component.
|
||||
/// Ordering is tool-centric: `compare_semver(tool, required)` → `Less` means tool is older.
|
||||
pub fn compare_semver(left: &str, right: &str) -> Option<std::cmp::Ordering> {
|
||||
let l = semver_numeric_parts(left)?;
|
||||
let r = semver_numeric_parts(right)?;
|
||||
Some(l.cmp(&r))
|
||||
}
|
||||
|
||||
fn semver_numeric_parts(version: &str) -> Option<(u64, u64, u64)> {
|
||||
let base = version
|
||||
.trim()
|
||||
.split_once('-')
|
||||
.map(|(h, _)| h)
|
||||
.unwrap_or(version.trim());
|
||||
if base.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut parts = base.split('.');
|
||||
let major = parts.next()?.parse::<u64>().ok()?;
|
||||
let minor = parts.next().unwrap_or("0").parse::<u64>().unwrap_or(0);
|
||||
let patch = parts
|
||||
.next()
|
||||
.unwrap_or("0")
|
||||
.chars()
|
||||
.take_while(|c| c.is_ascii_digit())
|
||||
.collect::<String>()
|
||||
.parse::<u64>()
|
||||
.unwrap_or(0);
|
||||
Some((major, minor, patch))
|
||||
}
|
||||
|
||||
/// Parses a version string into VersionInfo struct.
|
||||
pub fn parse_version_info(version: &str) -> VersionInfo {
|
||||
let mut full = version.trim().to_string();
|
||||
if full.is_empty() {
|
||||
@@ -55,6 +94,7 @@ pub fn parse_version_info(version: &str) -> VersionInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Substitutes profile, name, and version variables into a template string.
|
||||
pub fn substitute_vars(template: &str, info: &VersionInfo, profile: &str, name: &str) -> String {
|
||||
let s = template
|
||||
.replace("{profile}", profile)
|
||||
@@ -62,6 +102,7 @@ pub fn substitute_vars(template: &str, info: &VersionInfo, profile: &str, name:
|
||||
substitute_version_vars(&s, info)
|
||||
}
|
||||
|
||||
/// Substitutes version-specific variables into a template string.
|
||||
pub fn substitute_version_vars(template: &str, info: &VersionInfo) -> String {
|
||||
template
|
||||
.replace("{version}", &info.full)
|
||||
@@ -72,6 +113,7 @@ pub fn substitute_version_vars(template: &str, info: &VersionInfo) -> String {
|
||||
.replace("{version_suffix_dash}", &info.suffix_dash)
|
||||
}
|
||||
|
||||
/// Normalizes a target string to a relative path for artifact output directory.
|
||||
pub fn normalize_target(target: &str, profile: &str) -> Option<String> {
|
||||
let trimmed = normalize_target_os(target.trim());
|
||||
if trimmed.is_empty() {
|
||||
@@ -81,19 +123,111 @@ pub fn normalize_target(target: &str, profile: &str) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the native host target directory relative to project root for given profile.
|
||||
pub fn native_host_target_rel(profile: &str) -> PathBuf {
|
||||
if cfg!(any(
|
||||
target_os = "linux",
|
||||
target_os = "freebsd",
|
||||
target_os = "openbsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "dragonfly"
|
||||
)) {
|
||||
Path::new("target")
|
||||
.join(default_target_triple())
|
||||
.join(profile)
|
||||
} else {
|
||||
Path::new("target").join(profile)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the artifact output directory for a project/workspace build.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `project_root`: Package root.
|
||||
/// - `workspace_root`: If set, artifacts go under the workspace `target/`.
|
||||
/// - `profile`: Build profile directory segment.
|
||||
/// - `build_target`: Target triple or short name (may be empty).
|
||||
/// - `out_dir`: Explicit `out_dir` from config (wins when non-empty).
|
||||
/// - `has_explicit_target`: Whether CLI/config set a target (changes layout).
|
||||
///
|
||||
/// # Returns
|
||||
/// Absolute directory path as a string.
|
||||
pub fn resolve_artifact_target_dir(
|
||||
project_root: &Path,
|
||||
workspace_root: Option<&Path>,
|
||||
profile: &str,
|
||||
build_target: &str,
|
||||
out_dir: &str,
|
||||
has_explicit_target: bool,
|
||||
) -> String {
|
||||
if !out_dir.trim().is_empty() {
|
||||
let p = Path::new(out_dir.trim());
|
||||
return if p.is_absolute() {
|
||||
out_dir.trim().to_string()
|
||||
} else {
|
||||
project_root.join(p).to_string_lossy().into_owned()
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(ws) = workspace_root {
|
||||
let triple = if build_target.trim().is_empty() {
|
||||
default_target_triple()
|
||||
} else {
|
||||
let n = normalize_target_os(build_target.trim());
|
||||
if n.is_empty() {
|
||||
default_target_triple()
|
||||
} else {
|
||||
n.to_string()
|
||||
}
|
||||
};
|
||||
return ws
|
||||
.join("target")
|
||||
.join(triple)
|
||||
.join(profile)
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
}
|
||||
|
||||
let base = if has_explicit_target {
|
||||
match normalize_target(build_target, profile) {
|
||||
Some(rel) => PathBuf::from(rel),
|
||||
None => native_host_target_rel(profile),
|
||||
}
|
||||
} else {
|
||||
native_host_target_rel(profile)
|
||||
};
|
||||
project_root.join(base).to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
/// Normalizes kind string to standard value, defaulting to "bin" if empty.
|
||||
pub fn normalize_kind(kind: &str) -> &str {
|
||||
let trimmed = kind.trim();
|
||||
if trimmed.is_empty() { "bin" } else { trimmed }
|
||||
}
|
||||
|
||||
/// Checks if the kind is a flat binary artifact.
|
||||
pub fn is_flat_bin(kind: &str) -> bool {
|
||||
kind == "flat-bin"
|
||||
}
|
||||
|
||||
/// Returns true for kinds that skip the normal link/archive step: `none`, `custom`, or `flat-bin`.
|
||||
///
|
||||
/// Callers that still need a flat-binary path use [`is_flat_bin`] separately
|
||||
/// (e.g. `is_compile_only(kind) && !is_flat_bin(kind)` before an early return).
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `kind`: Normalized artifact kind string.
|
||||
///
|
||||
/// # Returns
|
||||
/// `true` for `none`, `custom`, or `flat-bin`.
|
||||
///
|
||||
/// Callers that still need a flat-binary link use [`is_flat_bin`] separately
|
||||
/// (e.g. `is_compile_only(kind) && !is_flat_bin(kind)` before early return).
|
||||
pub fn is_compile_only(kind: &str) -> bool {
|
||||
matches!(kind, "none" | "custom" | "flat-bin")
|
||||
}
|
||||
|
||||
/// Normalizes platform string, returning None if empty.
|
||||
pub fn normalize_platform(platform: &str) -> Option<&str> {
|
||||
let trimmed = platform.trim();
|
||||
if trimmed.is_empty() {
|
||||
@@ -103,22 +237,30 @@ pub fn normalize_platform(platform: &str) -> Option<&str> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Expands short OS names (`linux`/`macos`/`windows`) to default triples; leaves others as-is.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `target`: Short name, full triple, or empty.
|
||||
///
|
||||
/// # Returns
|
||||
/// Canonical triple string, empty input unchanged, unknown bare names returned with a warning.
|
||||
pub fn normalize_target_os(target: &str) -> &str {
|
||||
match target {
|
||||
"" => "",
|
||||
"linux" => "x86_64-unknown-linux-gnu",
|
||||
"macos" => "x86_64-apple-darwin",
|
||||
"windows" => "x86_64-pc-windows-msvc",
|
||||
_ if target.contains('-') => target,
|
||||
_ => {
|
||||
warn(&format!(
|
||||
"Unknown target '{}', using as-is. Supported short names: linux, macos, windows",
|
||||
target
|
||||
));
|
||||
warn!(
|
||||
"Unknown target '{target}', using as-is. Supported short names: linux, macos, windows"
|
||||
);
|
||||
target
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the default target triple for the current host platform.
|
||||
pub fn default_target_triple() -> String {
|
||||
let arch = std::env::consts::ARCH;
|
||||
if cfg!(target_os = "linux") {
|
||||
@@ -149,6 +291,7 @@ pub fn default_target_triple() -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepends a --target flag to flags vector if using Clang and not already present.
|
||||
pub fn prepend_clang_target_flag(flags: &mut Vec<String>, target: Option<&str>, tool: &str) {
|
||||
let Some(target) = target.map(str::trim).filter(|t| !t.is_empty()) else {
|
||||
return;
|
||||
@@ -166,6 +309,7 @@ pub fn prepend_clang_target_flag(flags: &mut Vec<String>, target: Option<&str>,
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves a string value from config, falling back to empty string if missing.
|
||||
pub fn get_config_str(config: &Config, key: &str) -> String {
|
||||
config
|
||||
.get(key)
|
||||
@@ -174,6 +318,7 @@ pub fn get_config_str(config: &Config, key: &str) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Retrieves profile-specific table from config.
|
||||
pub fn profile_table<'a>(config: &'a Config, profile: &str) -> Option<&'a toml::value::Table> {
|
||||
config
|
||||
.get("build")
|
||||
@@ -182,6 +327,7 @@ pub fn profile_table<'a>(config: &'a Config, profile: &str) -> Option<&'a toml::
|
||||
.and_then(|v| v.as_table())
|
||||
}
|
||||
|
||||
/// Retrieves an optional string value from config.
|
||||
pub fn get_config_opt(config: &Config, key: &str) -> Option<String> {
|
||||
let value = config.get(key)?.as_str()?;
|
||||
let trimmed = value.trim();
|
||||
@@ -192,6 +338,7 @@ pub fn get_config_opt(config: &Config, key: &str) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves string value with profile override support.
|
||||
pub fn get_string_with_profile(config: &Config, field: &str, profile: &str) -> String {
|
||||
let base = get_config_str(config, &format!("build.{field}"));
|
||||
let Some(table) = profile_table(config, profile) else {
|
||||
@@ -206,6 +353,7 @@ pub fn get_string_with_profile(config: &Config, field: &str, profile: &str) -> S
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves list value from config.
|
||||
pub fn get_config_list(config: &Config, key: &str) -> Vec<String> {
|
||||
config
|
||||
.get(key)
|
||||
@@ -219,6 +367,7 @@ pub fn get_config_list(config: &Config, key: &str) -> Vec<String> {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Retrieves list value with profile override support.
|
||||
pub fn get_list_with_profile(config: &Config, field: &str, profile: &str) -> Vec<String> {
|
||||
let mut out = get_config_list(config, &format!("build.{field}"));
|
||||
if let Some(table) = profile_table(config, profile)
|
||||
@@ -234,6 +383,7 @@ pub fn get_list_with_profile(config: &Config, field: &str, profile: &str) -> Vec
|
||||
out
|
||||
}
|
||||
|
||||
/// Helper function to check if a string contains a pattern ignoring case.
|
||||
fn contains_ignore_case(text: &str, pattern: &str) -> bool {
|
||||
if pattern.len() > text.len() {
|
||||
return false;
|
||||
@@ -243,6 +393,7 @@ fn contains_ignore_case(text: &str, pattern: &str) -> bool {
|
||||
.any(|w| w.eq_ignore_ascii_case(pattern.as_bytes()))
|
||||
}
|
||||
|
||||
/// Checks if target is a bare-metal or embedded target.
|
||||
pub fn is_bare_metal_target(target: Option<&str>) -> bool {
|
||||
match target {
|
||||
Some(t) => {
|
||||
@@ -256,6 +407,7 @@ pub fn is_bare_metal_target(target: Option<&str>) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns default flags for a given profile.
|
||||
pub fn default_profile_flags(profile: &str) -> &'static [&'static str] {
|
||||
match profile {
|
||||
"release" => &["-O3", "-DNDEBUG"],
|
||||
@@ -271,6 +423,7 @@ pub fn default_profile_flags(profile: &str) -> &'static [&'static str] {
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves boolean value with profile override support.
|
||||
pub fn get_bool_with_profile(config: &Config, field: &str, profile: &str, default: bool) -> bool {
|
||||
let base = config
|
||||
.get(&format!("build.{field}"))
|
||||
@@ -280,6 +433,7 @@ pub fn get_bool_with_profile(config: &Config, field: &str, profile: &str, defaul
|
||||
profile_val.or(base).unwrap_or(default)
|
||||
}
|
||||
|
||||
/// Retrieves language from config with profile support.
|
||||
pub fn get_language_with_profile(config: &Config, profile: &str) -> Result<String, String> {
|
||||
if let Some(table) = profile_table(config, profile)
|
||||
&& let Some(value) = table.get("language")
|
||||
@@ -292,10 +446,12 @@ pub fn get_language_with_profile(config: &Config, profile: &str) -> Result<Strin
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves language from config with profile support, defaulting to "c" on error.
|
||||
pub fn get_language_with_profile_or_default(config: &Config, profile: &str) -> String {
|
||||
get_language_with_profile(config, profile).unwrap_or_else(|_| "c".to_string())
|
||||
}
|
||||
|
||||
/// Parses language value from config, supporting string or array format.
|
||||
pub fn parse_language_value(value: &toml::Value, key: &str) -> Result<String, String> {
|
||||
if let Some(s) = value.as_str() {
|
||||
let trimmed = s.trim();
|
||||
@@ -324,6 +480,7 @@ pub fn parse_language_value(value: &toml::Value, key: &str) -> Result<String, St
|
||||
Ok(parts.join(","))
|
||||
}
|
||||
|
||||
/// Resolves compiler name based on language, environment, and toolchain overrides.
|
||||
pub fn resolve_compiler(
|
||||
language: &str,
|
||||
compiler: &str,
|
||||
@@ -342,6 +499,7 @@ pub fn resolve_compiler(
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks environment variables for compiler override.
|
||||
fn env_override_compiler(lang: &str) -> Option<String> {
|
||||
if let Ok(value) = std::env::var("DCR_COMPILER") {
|
||||
let trimmed = value.trim();
|
||||
@@ -375,6 +533,7 @@ fn env_override_compiler(lang: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Checks toolchain overrides for compiler.
|
||||
fn toolchain_override_compiler(
|
||||
lang: &str,
|
||||
tc_cc: Option<&str>,
|
||||
@@ -393,6 +552,7 @@ fn toolchain_override_compiler(
|
||||
tc_cc.map(|v| v.to_string())
|
||||
}
|
||||
|
||||
/// Maps assembler compiler name to canonical form.
|
||||
fn map_asm_compiler(compiler: &str) -> String {
|
||||
match compiler.to_lowercase().as_str() {
|
||||
"gas" | "gnu-as" => "as".to_string(),
|
||||
@@ -403,6 +563,7 @@ fn map_asm_compiler(compiler: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines primary language from comma-separated list.
|
||||
pub fn primary_language(language: &str) -> String {
|
||||
let tokens: Vec<&str> = language
|
||||
.split(',')
|
||||
@@ -421,6 +582,7 @@ pub fn primary_language(language: &str) -> String {
|
||||
language.to_lowercase()
|
||||
}
|
||||
|
||||
/// Resolves tool from environment variable or fallback.
|
||||
pub fn resolve_tool(env_key: &str, fallback: Option<&str>) -> Option<String> {
|
||||
if let Ok(value) = std::env::var(env_key) {
|
||||
let trimmed = value.trim();
|
||||
@@ -431,6 +593,7 @@ pub fn resolve_tool(env_key: &str, fallback: Option<&str>) -> Option<String> {
|
||||
fallback.map(|v| v.to_string())
|
||||
}
|
||||
|
||||
/// Resolves pkg-config flags for packages.
|
||||
pub fn resolve_pkg_config_flags(
|
||||
pkgs: &[String],
|
||||
base_cflags: &[String],
|
||||
@@ -447,6 +610,7 @@ pub fn resolve_pkg_config_flags(
|
||||
Ok((cflags, ldflags))
|
||||
}
|
||||
|
||||
/// Resolves pkg-config flags, falling back on error.
|
||||
pub fn resolve_pkg_config_flags_lossy(
|
||||
pkgs: &[String],
|
||||
base_cflags: &[String],
|
||||
@@ -461,6 +625,7 @@ pub fn resolve_pkg_config_flags_lossy(
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs pkg-config command and returns output.
|
||||
pub fn run_pkg_config(pkg: &str, arg: &str) -> Result<String, String> {
|
||||
let output = std::process::Command::new("pkg-config")
|
||||
.arg(arg)
|
||||
@@ -474,6 +639,7 @@ pub fn run_pkg_config(pkg: &str, arg: &str) -> Result<String, String> {
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
// The split_flags function parses pkg-config output, handling quoted strings and whitespace to split into individual flags.
|
||||
fn split_flags(value: &str) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
let mut current = String::new();
|
||||
@@ -513,8 +679,10 @@ fn split_flags(value: &str) -> Vec<String> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Test for normalize_target_short_names function.
|
||||
#[test]
|
||||
fn normalize_target_short_names() {
|
||||
assert_eq!(normalize_target_os(""), "");
|
||||
assert_eq!(normalize_target_os("linux"), "x86_64-unknown-linux-gnu");
|
||||
assert_eq!(normalize_target_os("macos"), "x86_64-apple-darwin");
|
||||
assert_eq!(normalize_target_os("windows"), "x86_64-pc-windows-msvc");
|
||||
@@ -525,20 +693,31 @@ mod tests {
|
||||
assert_eq!(normalize_target_os("unknown"), "unknown");
|
||||
}
|
||||
|
||||
/// Test for default_target_triple_uses_host_arch function.
|
||||
#[test]
|
||||
fn default_target_triple_uses_host_arch() {
|
||||
let target = default_target_triple();
|
||||
if cfg!(target_os = "linux") {
|
||||
let env = if cfg!(target_env = "musl") {
|
||||
"musl"
|
||||
} else {
|
||||
"gnu"
|
||||
};
|
||||
assert_eq!(
|
||||
target,
|
||||
format!("{}-unknown-linux-gnu", std::env::consts::ARCH)
|
||||
format!("{}-unknown-linux-{env}", std::env::consts::ARCH)
|
||||
);
|
||||
} else if cfg!(target_os = "macos") {
|
||||
assert_eq!(target, format!("{}-apple-darwin", std::env::consts::ARCH));
|
||||
} else if cfg!(target_os = "windows") {
|
||||
let env = if cfg!(target_env = "gnu") {
|
||||
"gnu"
|
||||
} else {
|
||||
"msvc"
|
||||
};
|
||||
assert_eq!(
|
||||
target,
|
||||
format!("{}-pc-windows-msvc", std::env::consts::ARCH)
|
||||
format!("{}-pc-windows-{env}", std::env::consts::ARCH)
|
||||
);
|
||||
} else if cfg!(any(
|
||||
target_os = "freebsd",
|
||||
@@ -559,6 +738,28 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Test for resolve_artifact_target_dir_native_and_workspace function.
|
||||
#[test]
|
||||
fn resolve_artifact_target_dir_native_and_workspace() {
|
||||
let root = Path::new("/proj");
|
||||
let ws = Path::new("/ws");
|
||||
let host = default_target_triple();
|
||||
let native = resolve_artifact_target_dir(root, None, "debug", &host, "", false);
|
||||
let expected_native = root.join(native_host_target_rel("debug"));
|
||||
assert_eq!(native, expected_native.to_string_lossy());
|
||||
|
||||
let explicit =
|
||||
resolve_artifact_target_dir(root, None, "debug", "x86_64-unknown-linux-gnu", "", true);
|
||||
assert!(explicit.contains("x86_64-unknown-linux-gnu"));
|
||||
|
||||
let ws_dir = resolve_artifact_target_dir(root, Some(ws), "debug", &host, "", false);
|
||||
assert!(ws_dir.starts_with("/ws") || ws_dir.contains("ws"));
|
||||
assert!(ws_dir.contains(&host) || cfg!(windows));
|
||||
let out = resolve_artifact_target_dir(root, None, "debug", "", "dist", false);
|
||||
assert!(out.contains("dist"));
|
||||
}
|
||||
|
||||
/// Test for prepend_clang_target_flag_adds_once_for_clang function.
|
||||
#[test]
|
||||
fn prepend_clang_target_flag_adds_once_for_clang() {
|
||||
let mut flags = vec!["-O2".to_string()];
|
||||
@@ -573,6 +774,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Test for prepend_clang_target_flag_ignores_non_clang_tools function.
|
||||
#[test]
|
||||
fn prepend_clang_target_flag_ignores_non_clang_tools() {
|
||||
let mut flags = vec!["-O2".to_string()];
|
||||
@@ -580,6 +782,7 @@ mod tests {
|
||||
assert_eq!(flags, vec!["-O2".to_string()]);
|
||||
}
|
||||
|
||||
/// Test for parse_version_parts function.
|
||||
#[test]
|
||||
fn parse_version_parts() {
|
||||
let info = parse_version_info("1.2.3-beta");
|
||||
@@ -591,19 +794,32 @@ mod tests {
|
||||
assert_eq!(info.suffix_dash, "-beta");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compare_semver_orders_and_ignores_prerelease() {
|
||||
use std::cmp::Ordering;
|
||||
assert_eq!(compare_semver("0.8.4", "0.8.4"), Some(Ordering::Equal));
|
||||
assert_eq!(compare_semver("0.8.3", "0.8.4"), Some(Ordering::Less));
|
||||
assert_eq!(compare_semver("0.9.0", "0.8.4"), Some(Ordering::Greater));
|
||||
assert_eq!(compare_semver("1.0.0-dev", "1.0.0"), Some(Ordering::Equal));
|
||||
assert_eq!(compare_semver("not-a-version", "0.1.0"), None);
|
||||
}
|
||||
|
||||
/// Test for normalize_target_with_profile function.
|
||||
#[test]
|
||||
fn normalize_target_with_profile() {
|
||||
let linux = normalize_target_os("linux");
|
||||
assert_eq!(
|
||||
normalize_target("linux", "debug"),
|
||||
Some("target/x86_64-unknown-linux-gnu/debug".into())
|
||||
Some(format!("target/{linux}/debug"))
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_target("x86_64-unknown-linux-gnu", "release"),
|
||||
Some("target/x86_64-unknown-linux-gnu/release".into())
|
||||
normalize_target(linux, "release"),
|
||||
Some(format!("target/{linux}/release"))
|
||||
);
|
||||
assert_eq!(normalize_target("", "debug"), None);
|
||||
}
|
||||
|
||||
/// Test for normalize_kind_empty_defaults_to_bin function.
|
||||
#[test]
|
||||
fn normalize_kind_empty_defaults_to_bin() {
|
||||
assert_eq!(normalize_kind(""), "bin");
|
||||
@@ -612,6 +828,7 @@ mod tests {
|
||||
assert_eq!(normalize_kind(" elf "), "elf");
|
||||
}
|
||||
|
||||
/// Test for normalize_platform_empty_is_none function.
|
||||
#[test]
|
||||
fn normalize_platform_empty_is_none() {
|
||||
assert_eq!(normalize_platform(""), None);
|
||||
@@ -620,6 +837,7 @@ mod tests {
|
||||
assert_eq!(normalize_platform(" efi "), Some("efi"));
|
||||
}
|
||||
|
||||
/// Test for default_profile_flags_by_profile function.
|
||||
#[test]
|
||||
fn default_profile_flags_by_profile() {
|
||||
assert!(default_profile_flags("debug").contains(&"-O0"));
|
||||
@@ -629,6 +847,7 @@ mod tests {
|
||||
assert!(default_profile_flags("unknown").is_empty());
|
||||
}
|
||||
|
||||
/// Test for is_bare_metal_detects_embedded_targets function.
|
||||
#[test]
|
||||
fn is_bare_metal_detects_embedded_targets() {
|
||||
assert!(is_bare_metal_target(Some("aarch64-none-elf")));
|
||||
|
||||
@@ -15,35 +15,33 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub const RESET: &str = "\x1b[0m";
|
||||
pub use owo_colors::{OwoColorize as Colorize, Style};
|
||||
|
||||
// Сli theme styles
|
||||
// Main
|
||||
#[allow(dead_code)]
|
||||
pub const BOLD: &str = "\x1b[1m";
|
||||
pub const BOLD_RED: Style = Style::new().bright_red().bold();
|
||||
#[allow(dead_code)]
|
||||
pub const BOLD_GREEN: Style = Style::new().bright_green().bold();
|
||||
#[allow(dead_code)]
|
||||
pub const BOLD_YELLOW: Style = Style::new().bright_yellow().bold();
|
||||
#[allow(dead_code)]
|
||||
pub const BOLD_CYAN: Style = Style::new().bright_cyan().bold();
|
||||
#[allow(dead_code)]
|
||||
pub const BOLD_BLUE: Style = Style::new().bright_blue().bold();
|
||||
|
||||
// Help
|
||||
#[allow(dead_code)]
|
||||
pub const BRIGHT_RED: &str = "\x1b[91m";
|
||||
pub const HELP_SECTION_TITLE_ST: Style = BOLD_GREEN;
|
||||
#[allow(dead_code)]
|
||||
pub const BRIGHT_GREEN: &str = "\x1b[92m";
|
||||
#[allow(dead_code)]
|
||||
pub const BRIGHT_YELLOW: &str = "\x1b[93m";
|
||||
#[allow(dead_code)]
|
||||
pub const BRIGHT_CYAN: &str = "\x1b[96m";
|
||||
#[allow(dead_code)]
|
||||
pub const BOLD_RED: &str = "\x1b[1m\x1b[91m";
|
||||
#[allow(dead_code)]
|
||||
pub const BOLD_GREEN: &str = "\x1b[1m\x1b[92m";
|
||||
#[allow(dead_code)]
|
||||
pub const BOLD_YELLOW: &str = "\x1b[1m\x1b[93m";
|
||||
#[allow(dead_code)]
|
||||
pub const BOLD_CYAN: &str = "\x1b[1m\x1b[96m";
|
||||
pub const HELP_EXAMPLES_ST: Style = BOLD_CYAN;
|
||||
|
||||
// Other
|
||||
#[allow(dead_code)]
|
||||
pub fn colored(msg: &str, style: &str) -> String {
|
||||
format!("{style}{msg}{RESET}")
|
||||
}
|
||||
|
||||
pub const ERROR_ST: Style = BOLD_RED;
|
||||
#[allow(dead_code)]
|
||||
pub fn printc(msg: &str, style: &str) {
|
||||
println!("{style}{msg}{RESET}");
|
||||
}
|
||||
pub const SUCCESS_ST: Style = BOLD_GREEN;
|
||||
#[allow(dead_code)]
|
||||
pub const ALERT_ST: Style = BOLD_YELLOW;
|
||||
#[allow(dead_code)]
|
||||
pub const SKIP_ST: Style = BOLD_BLUE;
|
||||
189
src/utils/fs.rs
189
src/utils/fs.rs
@@ -16,8 +16,13 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Resolves the path to the user's home directory across different operating systems.
|
||||
///
|
||||
/// Checks the `HOME` environment variable (Unix-like systems) first,
|
||||
/// then falls back to `USERPROFILE` (Windows).
|
||||
pub fn home_dir() -> Option<PathBuf> {
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
return Some(PathBuf::from(home));
|
||||
@@ -28,6 +33,7 @@ pub fn home_dir() -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Converts a byte slice into a lowercase hexadecimal string.
|
||||
pub fn to_hex(bytes: &[u8]) -> String {
|
||||
let mut out = String::with_capacity(bytes.len() * 2);
|
||||
for b in bytes {
|
||||
@@ -36,6 +42,7 @@ pub fn to_hex(bytes: &[u8]) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
/// Lists all entry names (files and directories) inside a target directory.
|
||||
#[allow(dead_code)]
|
||||
pub fn check_dir(dir: Option<&str>) -> io::Result<Vec<String>> {
|
||||
let path: PathBuf = match dir {
|
||||
@@ -52,6 +59,7 @@ pub fn check_dir(dir: Option<&str>) -> io::Result<Vec<String>> {
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
/// Searches upwards from a starting path to locate the root directory containing `dcr.toml`.
|
||||
pub fn find_project_root(start: &Path) -> io::Result<Option<PathBuf>> {
|
||||
let mut current = start.to_path_buf();
|
||||
loop {
|
||||
@@ -65,6 +73,15 @@ pub fn find_project_root(start: &Path) -> io::Result<Option<PathBuf>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Executes a closure inside a specified directory and restores the original cwd afterwards.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `dir`: Directory to `cd` into for the duration of `f`.
|
||||
/// - `f`: Work to run with that cwd (its `Result` is returned as-is).
|
||||
///
|
||||
/// # Returns
|
||||
/// The closure's result, or an error if getting/changing directory fails.
|
||||
/// Cwd is restored even when `f` returns `Err`.
|
||||
pub fn with_dir<F, T>(dir: &Path, f: F) -> Result<T, String>
|
||||
where
|
||||
F: FnOnce() -> Result<T, String>,
|
||||
@@ -72,6 +89,178 @@ where
|
||||
let prev = std::env::current_dir().map_err(|_| "Failed to get current dir".to_string())?;
|
||||
std::env::set_current_dir(dir).map_err(|_| "Failed to change directory".to_string())?;
|
||||
let result = f();
|
||||
// Restore previous working directory regardless of closure result.
|
||||
let _ = std::env::set_current_dir(prev);
|
||||
result
|
||||
}
|
||||
|
||||
/// Strips Windows verbatim/extended-length prefixes (e.g. `\\?\`, `\\.\`) and standardizes slashes.
|
||||
///
|
||||
/// Normalize path separators for cross-platform safety and removes platform-specific extended prefixes.
|
||||
pub fn strip_verbatim_prefix(path: PathBuf) -> PathBuf {
|
||||
let s = path.to_string_lossy();
|
||||
let mut cleaned = s.replace('\\', "/");
|
||||
if let Some(rest) = cleaned.strip_prefix("//?/") {
|
||||
cleaned = rest.to_string();
|
||||
} else if let Some(rest) = cleaned.strip_prefix("//./") {
|
||||
cleaned = rest.to_string();
|
||||
} else if let Some(rest) = cleaned.strip_prefix(r"\\?\") {
|
||||
cleaned = rest.replace('\\', "/");
|
||||
} else if let Some(rest) = cleaned.strip_prefix(r"\\.\") {
|
||||
cleaned = rest.replace('\\', "/");
|
||||
}
|
||||
if cleaned.len() >= 3 && cleaned.as_bytes()[0] == b'/' && cleaned.as_bytes()[2] == b':' {
|
||||
cleaned = cleaned[1..].to_string();
|
||||
}
|
||||
if cfg!(windows) {
|
||||
PathBuf::from(cleaned.replace('/', "\\"))
|
||||
} else {
|
||||
PathBuf::from(cleaned)
|
||||
}
|
||||
}
|
||||
|
||||
/// Canonicalizes a path to its absolute form and cleans up Windows extended-length prefixes.
|
||||
///
|
||||
/// Falls back to joining relative paths with the current working directory if canonicalization fails.
|
||||
pub fn canonicalize_path(path: &Path) -> PathBuf {
|
||||
let raw = std::fs::canonicalize(path).unwrap_or_else(|_| {
|
||||
if path.is_absolute() {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
std::env::current_dir()
|
||||
.map(|cwd| cwd.join(path))
|
||||
.unwrap_or_else(|_| path.to_path_buf())
|
||||
}
|
||||
});
|
||||
strip_verbatim_prefix(raw)
|
||||
}
|
||||
|
||||
/// Resolves a path relative to a root directory and returns a canonicalized absolute path.
|
||||
pub fn absolute_join(root: &Path, path: &Path) -> PathBuf {
|
||||
let joined = if path.is_absolute() {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
root.join(path)
|
||||
};
|
||||
canonicalize_path(&joined)
|
||||
}
|
||||
|
||||
/// Ensures the `.dcr` internal metadata directory and its `ide` subfolder exist within a project root.
|
||||
pub fn ensure_dcr_dir(root: &Path) -> io::Result<PathBuf> {
|
||||
let dcr = root.join(".dcr");
|
||||
std::fs::create_dir_all(dcr.join("ide"))?;
|
||||
Ok(dcr)
|
||||
}
|
||||
|
||||
/// Writes `contents` via a `.tmp.<pid>` file then renames into place (best-effort replace).
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `path`: Final destination path (parent dirs are created).
|
||||
/// - `contents`: Bytes to write.
|
||||
///
|
||||
/// # Returns
|
||||
/// `Ok(())` after a successful rename (with fallbacks if the target already exists).
|
||||
pub fn atomic_write(path: &Path, contents: impl AsRef<[u8]>) -> io::Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let pid = std::process::id();
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| "tmp".to_string());
|
||||
let tmp = path.with_file_name(format!("{file_name}.tmp.{pid}"));
|
||||
{
|
||||
let mut f = std::fs::File::create(&tmp)?;
|
||||
f.write_all(contents.as_ref())?;
|
||||
let _ = f.sync_all();
|
||||
}
|
||||
match std::fs::rename(&tmp, path) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) => {
|
||||
// Handle cross-device moves or existing file replacement fallbacks.
|
||||
if path.exists() {
|
||||
let _ = std::fs::remove_file(path);
|
||||
match std::fs::rename(&tmp, path) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e2) => {
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
Err(e2)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensures that `.gitignore` exists in the project directory and includes the `.dcr/` output rule.
|
||||
///
|
||||
/// If `.gitignore` does not exist, creates one with standard `/target` and `.dcr/` entries.
|
||||
pub fn ensure_gitignore_has_dcr(project_dir: &Path) -> io::Result<()> {
|
||||
let path = project_dir.join(".gitignore");
|
||||
if path.exists() {
|
||||
let content = std::fs::read_to_string(&path)?;
|
||||
let has = content.lines().any(|l| {
|
||||
let t = l.trim();
|
||||
t == ".dcr/" || t == ".dcr" || t == "/.dcr/" || t == "/.dcr"
|
||||
});
|
||||
if !has {
|
||||
let mut new_content = content;
|
||||
if !new_content.ends_with('\n') && !new_content.is_empty() {
|
||||
new_content.push('\n');
|
||||
}
|
||||
new_content.push_str(".dcr/\n");
|
||||
std::fs::write(&path, new_content)?;
|
||||
}
|
||||
} else {
|
||||
std::fs::write(&path, "/target\n.dcr/\n")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn canonicalize_path_absolute() {
|
||||
let cwd = std::env::current_dir().unwrap();
|
||||
let got = canonicalize_path(&cwd);
|
||||
assert!(got.is_absolute());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_path_relative_fallback() {
|
||||
let p = Path::new("does-not-exist-xyz-12345");
|
||||
let got = canonicalize_path(p);
|
||||
assert!(got.is_absolute());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn atomic_write_roundtrip() {
|
||||
let dir = std::env::temp_dir().join(format!("dcr_atomic_{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("out.json");
|
||||
atomic_write(&path, b"hello").unwrap();
|
||||
assert_eq!(fs::read_to_string(&path).unwrap(), "hello");
|
||||
atomic_write(&path, b"world").unwrap();
|
||||
assert_eq!(fs::read_to_string(&path).unwrap(), "world");
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_dcr_dir_creates_ide() {
|
||||
let dir = std::env::temp_dir().join(format!("dcr_dir_{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
let dcr = ensure_dcr_dir(&dir).unwrap();
|
||||
assert!(dcr.is_dir());
|
||||
assert!(dcr.join("ide").is_dir());
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::{path::Path, process::Command};
|
||||
|
||||
/// Checks if the `git` executable is available in the system's PATH.
|
||||
///
|
||||
/// This function executes `git --version` and returns `true` if the command succeeds, `false` otherwise.
|
||||
pub fn is_git_available() -> bool {
|
||||
Command::new("git")
|
||||
.arg("--version")
|
||||
@@ -9,6 +11,7 @@ pub fn is_git_available() -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Initializes a new Git repository at the specified path.
|
||||
pub fn git_init(path: &Path) -> Result<(), String> {
|
||||
if !is_git_available() {
|
||||
return Err("git executable not found in PATH".to_string());
|
||||
|
||||
@@ -15,14 +15,55 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::utils::text::{BOLD_RED, BOLD_YELLOW, colored};
|
||||
use env_logger::{Builder, WriteStyle};
|
||||
use log::{Level, LevelFilter};
|
||||
use owo_colors::OwoColorize;
|
||||
use std::io::Write;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn error(msg: &str) {
|
||||
eprintln!("{}: {msg}", colored("error", BOLD_RED));
|
||||
// A function for initializing the logging lib with basic settings.
|
||||
pub fn init() {
|
||||
Builder::new()
|
||||
.filter_level(LevelFilter::Warn)
|
||||
.write_style(WriteStyle::Auto)
|
||||
.format(|buf, record| {
|
||||
let colored_lv = match record.level() {
|
||||
Level::Error => record.level().red().to_string(),
|
||||
Level::Warn => record.level().yellow().to_string(),
|
||||
Level::Info => record.level().default_color().to_string(),
|
||||
_ => record.level().purple().italic().to_string(),
|
||||
};
|
||||
|
||||
writeln!(
|
||||
buf,
|
||||
"{}: {}",
|
||||
colored_lv.to_lowercase().bold(),
|
||||
record.args(),
|
||||
)
|
||||
})
|
||||
.init();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn warn(msg: &str) {
|
||||
eprintln!("{}: {msg}", colored("warn", BOLD_YELLOW));
|
||||
/// Prints message to stdout with the given `Style`. Copy of println! macro
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! sprintln {
|
||||
($style:expr, $($arg:tt)*) => {
|
||||
{
|
||||
use ::owo_colors::OwoColorize as _;
|
||||
println!("{}", format!($($arg)*).style($style));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Prints message to stdout with the given `Style`. Copy of print! macro
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! sprint {
|
||||
($style:expr, $($arg:tt)*) => {
|
||||
{
|
||||
use ::owo_colors::OwoColorize as _;
|
||||
print!("{}", format!($($arg)*).style($style));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use {sprint, sprintln};
|
||||
|
||||
@@ -14,9 +14,19 @@
|
||||
//
|
||||
// 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 utilities module providing access to build, filesystem, git, log, and text helpers.
|
||||
pub mod build;
|
||||
|
||||
/// File system path utilities and operations.
|
||||
pub mod fs;
|
||||
|
||||
/// Git version control system integration.
|
||||
pub mod git;
|
||||
|
||||
/// Logging utilities for application events and debugging.
|
||||
pub mod log;
|
||||
pub mod text;
|
||||
|
||||
/// Text processing and manipulation helpers.
|
||||
pub mod cli_styles;
|
||||
|
||||
@@ -87,6 +87,33 @@ fn build_run_clean_flags_normal_project() {
|
||||
"dcr run should start"
|
||||
);
|
||||
|
||||
std::fs::write(
|
||||
dir.join("src/main.c"),
|
||||
r#"#include <stdio.h>
|
||||
int main(int argc, char **argv) {
|
||||
for (int i = 1; i < argc; i++) {
|
||||
printf("ARG:%s\n", argv[i]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.expect("rewrite main.c");
|
||||
let out = run_dcr_env(&["run", "--force", "--", "--test_help", "foo"], &dir, &envs);
|
||||
let combined = format!(
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"dcr run -- <args> should succeed:\n{combined}"
|
||||
);
|
||||
assert!(
|
||||
combined.contains("ARG:--test_help") && combined.contains("ARG:foo"),
|
||||
"binary should receive args after `--`:\n{combined}"
|
||||
);
|
||||
|
||||
let out = run_dcr_env(&["clean", "--release"], &dir, &envs);
|
||||
assert!(out.status.success(), "dcr clean --release should succeed");
|
||||
let release_dir = host_profile_dir(&dir, "release");
|
||||
@@ -129,15 +156,9 @@ fn build_with_target_config() {
|
||||
if !out.status.success() {
|
||||
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
|
||||
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
if cfg!(windows) {
|
||||
eprintln!("skipping linux-target path assert: build failed (no linux cross toolchain)");
|
||||
return;
|
||||
}
|
||||
eprintln!("skipping linux-target path assert: build failed (no suitable toolchain)");
|
||||
return;
|
||||
}
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"dcr build with target = \"linux\" should succeed"
|
||||
);
|
||||
|
||||
let out_dir = dir
|
||||
.join("target")
|
||||
@@ -237,13 +258,11 @@ roots = ["src"]
|
||||
}
|
||||
assert!(out.status.success(), "flat-bin nasm build should succeed");
|
||||
|
||||
let candidates = [
|
||||
host_profile_dir(&dir, "debug").join("boot.bin"),
|
||||
dir.join("target/debug/boot.bin"),
|
||||
];
|
||||
let boot = host_profile_dir(&dir, "debug").join("boot.bin");
|
||||
assert!(
|
||||
candidates.iter().any(|p| p.is_file()),
|
||||
"expected boot.bin under host profile dir, candidates: {candidates:?}"
|
||||
boot.is_file(),
|
||||
"expected boot.bin under host profile dir: {}",
|
||||
boot.display()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -269,12 +288,9 @@ fn package_build_target_used_without_cli_flag() {
|
||||
if !out.status.success() {
|
||||
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
|
||||
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
if cfg!(windows) {
|
||||
eprintln!("skipping linux-target path assert: build failed (no linux cross toolchain)");
|
||||
return;
|
||||
}
|
||||
eprintln!("skipping package-target path assert: build failed (no suitable toolchain)");
|
||||
return;
|
||||
}
|
||||
assert!(out.status.success(), "build should use package target");
|
||||
|
||||
let out_dir = dir
|
||||
.join("target")
|
||||
@@ -286,3 +302,57 @@ fn package_build_target_used_without_cli_flag() {
|
||||
"artifact should be under package build.target path",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_target_section_ldflags_apply_without_cli_target() {
|
||||
let Some(compiler) = available_compiler() else {
|
||||
eprintln!("no compiler found; skipping host target ldflags test");
|
||||
return;
|
||||
};
|
||||
|
||||
let dir = unique_sandbox_dir("host_ldflags");
|
||||
let out = run_dcr(&["init"], &dir);
|
||||
assert!(out.status.success(), "dcr init should succeed");
|
||||
|
||||
let host = if cfg!(target_os = "linux") {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let env = if cfg!(target_env = "musl") {
|
||||
"musl"
|
||||
} else {
|
||||
"gnu"
|
||||
};
|
||||
format!("{arch}-unknown-linux-{env}")
|
||||
} else if cfg!(target_os = "macos") {
|
||||
format!("{}-apple-darwin", std::env::consts::ARCH)
|
||||
} else if cfg!(target_os = "windows") {
|
||||
let env = if cfg!(target_env = "gnu") {
|
||||
"gnu"
|
||||
} else {
|
||||
"msvc"
|
||||
};
|
||||
format!("{}-pc-windows-{env}", std::env::consts::ARCH)
|
||||
} else {
|
||||
eprintln!("unsupported host for host_target_section test; skipping");
|
||||
return;
|
||||
};
|
||||
|
||||
let toml_path = dir.join("dcr.toml");
|
||||
let toml = std::fs::read_to_string(&toml_path).expect("read dcr.toml");
|
||||
let updated = format!(
|
||||
"{toml}\n[build.{host}]\nldflags = [\"-Wl,-rpath,/nonexistent-dcr-host-ldflags\"]\n"
|
||||
);
|
||||
std::fs::write(&toml_path, updated).expect("write dcr.toml");
|
||||
|
||||
let envs = [("DCR_COMPILER", compiler), ("DCR_DEBUG", "1")];
|
||||
let out = run_dcr_env(&["build", "--force"], &dir, &envs);
|
||||
let combined = format!(
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(
|
||||
combined.contains("-Wl,-rpath,/nonexistent-dcr-host-ldflags")
|
||||
|| combined.contains("rpath,/nonexistent-dcr-host-ldflags"),
|
||||
"host target section ldflags must be applied on native build:\n{combined}"
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user