mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-22 09:39:36 +00:00
Compare commits
14 Commits
v0.8.4
...
f3605a86a2
| Author | SHA1 | Date | |
|---|---|---|---|
| f3605a86a2 | |||
| 9d21294cb4 | |||
| 8609d12179 | |||
| b6ecb2aa79 | |||
| dd89109f92 | |||
| c6af6c5196 | |||
| b6153dbaac | |||
| 9e2b7a897a | |||
| 9add069879 | |||
| 5eb00037ea | |||
| f7353f9d03 | |||
| 05e49838f7 | |||
| b0e426c189 | |||
| 60d7420929 |
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@@ -6,7 +6,19 @@ on:
|
|||||||
- master
|
- master
|
||||||
- main
|
- main
|
||||||
- dev
|
- dev
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'tests/**'
|
||||||
|
- 'Cargo.toml'
|
||||||
|
- 'Cargo.lock'
|
||||||
|
- '.github/workflows/ci.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'tests/**'
|
||||||
|
- 'Cargo.toml'
|
||||||
|
- 'Cargo.lock'
|
||||||
|
- '.github/workflows/ci.yml'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
|||||||
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
|
||||||
59
.github/workflows/release.yml
vendored
59
.github/workflows/release.yml
vendored
@@ -45,6 +45,10 @@ on:
|
|||||||
description: "Update Homebrew tap"
|
description: "Update Homebrew tap"
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
run_s3:
|
||||||
|
description: "Mirror Release to Yandex Object Storage (S3)"
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -1254,3 +1258,58 @@ jobs:
|
|||||||
#
|
#
|
||||||
# publish-snap:
|
# publish-snap:
|
||||||
# if: ...
|
# if: ...
|
||||||
|
|
||||||
|
# ── Mirror Release to Yandex Object Storage (S3) ─────────────────────────
|
||||||
|
publish-s3:
|
||||||
|
if: |
|
||||||
|
(github.event_name == 'push' && needs.setup.outputs.prerelease == 'false') ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_s3 == 'true')
|
||||||
|
needs:
|
||||||
|
- setup
|
||||||
|
- create-release
|
||||||
|
- man-pages
|
||||||
|
- build-linux
|
||||||
|
- build-macos
|
||||||
|
- build-bsd
|
||||||
|
- build-windows
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ needs.setup.outputs.tag }}
|
||||||
|
|
||||||
|
- name: Prepare Directory and Download GitHub Release Assets
|
||||||
|
id: fetch-assets
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
TAG="${{ needs.setup.outputs.tag }}"
|
||||||
|
|
||||||
|
mkdir -p dist
|
||||||
|
cd dist
|
||||||
|
|
||||||
|
echo "Downloading assets from GitHub Release for tag: ${TAG}"
|
||||||
|
gh release download "${TAG}" --dir . --repo "${{ github.repository }}"
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
echo "Archiving source code..."
|
||||||
|
git archive --format=tar.gz --prefix="dcr-${TAG}/" "${TAG}" -o "dist/dcr-src-${TAG}.tar.gz"
|
||||||
|
git archive --format=zip --prefix="dcr-${TAG}/" "${TAG}" -o "dist/dcr-src-${TAG}.zip"
|
||||||
|
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Upload Everything to Yandex Object Storage
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_EC2_METADATA_DISABLED: true
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
TAG="${{ needs.setup.outputs.tag }}"
|
||||||
|
|
||||||
|
aws s3 sync dist/ "s3://${{ secrets.S3_BUCKET_NAME }}/${TAG}/" \
|
||||||
|
--endpoint-url https://storage.yandexcloud.net \
|
||||||
|
--region ru-central1
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ sandbox/**
|
|||||||
personal/
|
personal/
|
||||||
.*
|
.*
|
||||||
!.gitignore
|
!.gitignore
|
||||||
|
!.github/
|
||||||
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,5 +1,31 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [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"
|
## [0.8.4] - 2026-08-04 "Инфраструктура .dcr/ и интеграция с IDE / Internal .dcr/ Layout & IDE Integration"
|
||||||
|
|
||||||
### RU
|
### RU
|
||||||
|
|||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -154,7 +154,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dcr"
|
name = "dcr"
|
||||||
version = "0.8.4"
|
version = "0.8.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
"fatfs",
|
"fatfs",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dcr"
|
name = "dcr"
|
||||||
version = "0.8.4"
|
version = "0.8.5"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
|
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -216,6 +216,30 @@ Full docs at **[dcr.dexoron.su](https://dcr.dexoron.su)** or in the [`docs/`](do
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Repository Mirrors
|
||||||
|
|
||||||
|
The primary repository is hosted on GitHub. The following mirrors are available for source code and releases:
|
||||||
|
|
||||||
|
| Region | Mirror | Contents |
|
||||||
|
|---|---|---|
|
||||||
|
| Russia | [GitVerse](https://gitverse.ru/dcr_labs/dcr) **(recommended)** | Source code |
|
||||||
|
| Russia | [SourceCraft](https://sourcecraft.dev/dexoron-dev/dcr) | Source code |
|
||||||
|
| Worldwide | [SourceForge](https://sourceforge.net/projects/dcr-tool/) **(recommended)** | Source code and releases |
|
||||||
|
| Worldwide | [GitLab](https://gitlab.com/dexoron-dev/dcr-tool/dcr) | Source code and releases |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VS Code Extension
|
||||||
|
|
||||||
|
Install the recommended [DCR extension by Dexoron-Labs](https://marketplace.visualstudio.com/items?itemName=Dexoron-Labs.dcr) for VS Code. Do not install Microsoft C/C++ extensions for DCR projects; DCR uses `clangd` instead. If you need debugging with Microsoft's debugger, use MS C/C++ only for its debugger component.
|
||||||
|
|
||||||
|
Recommended companion extensions:
|
||||||
|
|
||||||
|
- [Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml)
|
||||||
|
- [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb), or MS C/C++ for debugger support only
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|||||||
@@ -4,6 +4,16 @@ sidebar_label: Changelog
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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)
|
## 0.8.4 (2026-08-04)
|
||||||
|
|
||||||
**Added:**
|
**Added:**
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ sidebar_label: IDE Integration
|
|||||||
|
|
||||||
## VS Code
|
## VS Code
|
||||||
|
|
||||||
|
Install the recommended [DCR extension by Dexoron-Labs](https://marketplace.visualstudio.com/items?itemName=Dexoron-Labs.dcr). Do not install Microsoft C/C++ extensions for DCR projects: DCR uses `clangd`. MS C/C++ is acceptable only when you need its debugger.
|
||||||
|
|
||||||
|
Recommended companion extensions:
|
||||||
|
|
||||||
|
- [Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml)
|
||||||
|
- [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb), or MS C/C++ for debugger support only
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dcr gen vscode
|
dcr gen vscode
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ Main project configuration file. Located at the project root.
|
|||||||
| Field | Required | Description |
|
| Field | Required | Description |
|
||||||
|-------|----------|-------------|
|
|-------|----------|-------------|
|
||||||
| `name` | yes | Project name |
|
| `name` | yes | Project name |
|
||||||
| `version` | yes | Semantic version |
|
| `version` | yes | Semantic version of the **project** |
|
||||||
|
| `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"`) |
|
| `type` | no | `app`, `lib`, `none` (defaults to `"none"`) |
|
||||||
| `license` | no | SPDX license identifier |
|
| `license` | no | SPDX license identifier |
|
||||||
| `author` | no | Author |
|
| `author` | no | Author |
|
||||||
@@ -52,11 +53,26 @@ Main project configuration file. Located at the project root.
|
|||||||
[package]
|
[package]
|
||||||
name = "my-app"
|
name = "my-app"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dcr-version = "0.8.4"
|
||||||
type = "app"
|
type = "app"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
author = "John Doe"
|
author = "John Doe"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `package.dcr-version`
|
||||||
|
|
||||||
|
Optional pin of the DCR CLI the project was written for. On every `dcr.toml` load:
|
||||||
|
|
||||||
|
| Situation | Behaviour |
|
||||||
|
|-----------|-----------|
|
||||||
|
| Field missing / empty | Warning: add `dcr-version` (set by `dcr new` / `dcr init`) |
|
||||||
|
| Running dcr **older** than pin | Warning: upgrade dcr (`dcr --update`) |
|
||||||
|
| Running dcr **newer** than pin | Warning: review changelog, bump the pin when ready |
|
||||||
|
| Equal | Silent |
|
||||||
|
| Invalid semver | Warning once, field ignored |
|
||||||
|
|
||||||
|
Never fails the command — warnings only. Pre-release suffixes (`1.0.0-dev`) are ignored for comparison (`1.0.0-dev` ≡ `1.0.0`).
|
||||||
|
|
||||||
## [build]
|
## [build]
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|||||||
@@ -154,6 +154,11 @@ Project name (required).
|
|||||||
.B package.version
|
.B package.version
|
||||||
Project version (default: \fB0.1.0\fR).
|
Project version (default: \fB0.1.0\fR).
|
||||||
.TP
|
.TP
|
||||||
|
.B package.dcr\-version
|
||||||
|
Semver of the DCR tool this project targets. Set automatically by
|
||||||
|
\fBdcr new\fR / \fBdcr init\fR. Missing pin, or an installed \fBdcr\fR older/newer
|
||||||
|
than the pin, prints a warning (commands still run).
|
||||||
|
.TP
|
||||||
.B build.language
|
.B build.language
|
||||||
Programming language: \fBc\fR or \fBc++\fR (default: \fBc\fR).
|
Programming language: \fBc\fR or \fBc++\fR (default: \fBc\fR).
|
||||||
.TP
|
.TP
|
||||||
|
|||||||
@@ -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!")
|
|
||||||
@@ -132,11 +132,17 @@ pub struct ArchiveLayout {
|
|||||||
pub to: String,
|
pub to: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Package metadata section (name, version, type).
|
/// Package metadata section (name, version, type, dcr-version).
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct PackageConfig {
|
pub struct PackageConfig {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
|
/// Minimum/target DCR tool version this project was authored for (`package.dcr-version`).
|
||||||
|
///
|
||||||
|
/// Compared against the running `dcr` binary (`CARGO_PKG_VERSION`) on config load.
|
||||||
|
/// Missing or empty means "no pin" — no warning is emitted.
|
||||||
|
#[serde(default, rename = "dcr-version")]
|
||||||
|
pub dcr_version: Option<String>,
|
||||||
#[serde(default, rename = "type")]
|
#[serde(default, rename = "type")]
|
||||||
pub pkg_type: Option<String>,
|
pub pkg_type: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -258,6 +264,7 @@ impl Config {
|
|||||||
doc,
|
doc,
|
||||||
};
|
};
|
||||||
cfg.validate()?;
|
cfg.validate()?;
|
||||||
|
cfg.warn_dcr_version_mismatch();
|
||||||
Ok(cfg)
|
Ok(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,6 +283,7 @@ impl Config {
|
|||||||
doc,
|
doc,
|
||||||
};
|
};
|
||||||
cfg.validate()?;
|
cfg.validate()?;
|
||||||
|
cfg.warn_dcr_version_mismatch();
|
||||||
Ok(cfg)
|
Ok(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,6 +299,68 @@ impl Config {
|
|||||||
self.typed.package.as_ref()
|
self.typed.package.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Warn once per config path about `package.dcr-version` vs this `dcr` binary.
|
||||||
|
///
|
||||||
|
/// - Missing/`""` pin → recommend adding `dcr-version`.
|
||||||
|
/// - Tool older than pin → project may use features this binary does not support.
|
||||||
|
/// - Tool newer than pin → project may rely on outdated defaults; consider bumping the pin.
|
||||||
|
///
|
||||||
|
/// Never fails the load; warnings only. Equal pin is silent.
|
||||||
|
fn warn_dcr_version_mismatch(&self) {
|
||||||
|
let Some(pkg) = self.typed.package.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let path_key = self
|
||||||
|
.path
|
||||||
|
.canonicalize()
|
||||||
|
.unwrap_or_else(|_| self.path.clone())
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned();
|
||||||
|
if !dcr_version_warn_once(&path_key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tool = env!("CARGO_PKG_VERSION");
|
||||||
|
let required = pkg
|
||||||
|
.dcr_version
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|s| !s.is_empty());
|
||||||
|
let Some(required) = required else {
|
||||||
|
crate::utils::log::warn(&format!(
|
||||||
|
"package.dcr-version is missing in {}. Add e.g. dcr-version = \"{tool}\" \
|
||||||
|
so collaborators know which dcr this project targets \
|
||||||
|
(written automatically by `dcr new` / `dcr init`).",
|
||||||
|
self.path.display()
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match crate::utils::build::compare_semver(tool, required) {
|
||||||
|
Some(std::cmp::Ordering::Less) => {
|
||||||
|
crate::utils::log::warn(&format!(
|
||||||
|
"this project requires dcr {required} (package.dcr-version), \
|
||||||
|
but you are running dcr {tool}. Some features may be missing or misbehave. \
|
||||||
|
Upgrade dcr (e.g. `dcr --update`) or lower package.dcr-version if intentional."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Some(std::cmp::Ordering::Greater) => {
|
||||||
|
crate::utils::log::warn(&format!(
|
||||||
|
"this project pins dcr {required} (package.dcr-version), \
|
||||||
|
but you are running newer dcr {tool}. Config/defaults may be outdated. \
|
||||||
|
Review the changelog and bump package.dcr-version when the project is verified."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Some(std::cmp::Ordering::Equal) => {}
|
||||||
|
None => {
|
||||||
|
crate::utils::log::warn(&format!(
|
||||||
|
"package.dcr-version = \"{required}\" is not a valid semver (expected X.Y.Z); ignoring"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
/// Returns the build configuration if present.
|
/// Returns the build configuration if present.
|
||||||
pub fn build_config(&self) -> Option<&BuildConfig> {
|
pub fn build_config(&self) -> Option<&BuildConfig> {
|
||||||
@@ -765,11 +835,13 @@ fn default_toml_text() -> String {
|
|||||||
.ok()
|
.ok()
|
||||||
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string()))
|
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string()))
|
||||||
.unwrap_or_else(|| "project".to_string());
|
.unwrap_or_else(|| "project".to_string());
|
||||||
|
let dcr_version = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
"[package]\n\
|
"[package]\n\
|
||||||
name = \"{name}\"\n\
|
name = \"{name}\"\n\
|
||||||
version = \"{DEFAULT_VERSION}\"\n\
|
version = \"{DEFAULT_VERSION}\"\n\
|
||||||
|
dcr-version = \"{dcr_version}\"\n\
|
||||||
type = \"none\"\n\
|
type = \"none\"\n\
|
||||||
description = \"\"\n\
|
description = \"\"\n\
|
||||||
author = \"\"\n\
|
author = \"\"\n\
|
||||||
@@ -785,6 +857,17 @@ fn default_toml_text() -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
fn set_doc_path(doc: &mut DocumentMut, path: &[&str], value: &Value) -> Result<(), ConfigError> {
|
||||||
let mut current = doc.as_table_mut();
|
let mut current = doc.as_table_mut();
|
||||||
for &key in &path[..path.len().saturating_sub(1)] {
|
for &key in &path[..path.len().saturating_sub(1)] {
|
||||||
@@ -937,11 +1020,21 @@ mod tests {
|
|||||||
let dir = temp_dir("typed_config");
|
let dir = temp_dir("typed_config");
|
||||||
let path = write_toml_file(
|
let path = write_toml_file(
|
||||||
&dir,
|
&dir,
|
||||||
"[package]\nname = \"typed\"\nversion = \"1.2.3\"\ntype = \"lib\"\n\n[build]\nlanguage = [\"c\", \"c++\"]\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"staticlib\"\ncflags = [\"-Wall\"]\n\n[dependencies]\nfoo = \"1.0.0\"\n",
|
"[package]\nname = \"typed\"\nversion = \"1.2.3\"\ndcr-version = \"0.8.4\"\ntype = \"lib\"\n\n[build]\nlanguage = [\"c\", \"c++\"]\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"staticlib\"\ncflags = [\"-Wall\"]\n\n[dependencies]\nfoo = \"1.0.0\"\n",
|
||||||
);
|
);
|
||||||
let config = Config::open(&path.to_string_lossy()).unwrap();
|
let config = Config::open(&path.to_string_lossy()).unwrap();
|
||||||
assert_eq!(config.package().unwrap().name, "typed");
|
assert_eq!(config.package().unwrap().name, "typed");
|
||||||
assert_eq!(config.typed().package.as_ref().unwrap().version, "1.2.3");
|
assert_eq!(config.typed().package.as_ref().unwrap().version, "1.2.3");
|
||||||
|
assert_eq!(
|
||||||
|
config
|
||||||
|
.typed()
|
||||||
|
.package
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.dcr_version
|
||||||
|
.as_deref(),
|
||||||
|
Some("0.8.4")
|
||||||
|
);
|
||||||
assert_eq!(config.build_config().unwrap().compiler, "clang");
|
assert_eq!(config.build_config().unwrap().compiler, "clang");
|
||||||
assert_eq!(config.build_config().unwrap().cflags, ["-Wall"]);
|
assert_eq!(config.build_config().unwrap().cflags, ["-Wall"]);
|
||||||
assert!(config.typed().dependencies.contains_key("foo"));
|
assert!(config.typed().dependencies.contains_key("foo"));
|
||||||
|
|||||||
@@ -31,6 +31,39 @@ pub struct VersionInfo {
|
|||||||
pub suffix_dash: String,
|
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.
|
/// Parses a version string into VersionInfo struct.
|
||||||
pub fn parse_version_info(version: &str) -> VersionInfo {
|
pub fn parse_version_info(version: &str) -> VersionInfo {
|
||||||
let mut full = version.trim().to_string();
|
let mut full = version.trim().to_string();
|
||||||
@@ -761,6 +794,16 @@ mod tests {
|
|||||||
assert_eq!(info.suffix_dash, "-beta");
|
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 for normalize_target_with_profile function.
|
||||||
#[test]
|
#[test]
|
||||||
fn normalize_target_with_profile() {
|
fn normalize_target_with_profile() {
|
||||||
|
|||||||
Reference in New Issue
Block a user