mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 10:08:30 +00:00
Compare commits
18 Commits
v0.8.5
...
d587eba5a4
| Author | SHA1 | Date | |
|---|---|---|---|
| d587eba5a4 | |||
| bfd2b9d251 | |||
| 1e3eb658dc | |||
| f5dad74498 | |||
| 3c25539c11 | |||
| 54653dfd80 | |||
| f3605a86a2 | |||
| 9d21294cb4 | |||
| 8609d12179 | |||
| b6ecb2aa79 | |||
| dd89109f92 | |||
| c6af6c5196 | |||
| b6153dbaac | |||
| 9e2b7a897a | |||
| 9add069879 | |||
| 5eb00037ea | |||
| f7353f9d03 | |||
| 05e49838f7 |
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/
|
||||
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,5 +1,37 @@
|
||||
# Changelog
|
||||
|
||||
## [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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -154,7 +154,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dcr"
|
||||
version = "0.8.5"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"ctrlc",
|
||||
"fatfs",
|
||||
|
||||
13
Cargo.toml
13
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dcr"
|
||||
version = "0.8.5"
|
||||
version = "0.9.0"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
|
||||
@@ -88,4 +88,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">
|
||||
|
||||
93
dcrup.ps1
93
dcrup.ps1
@@ -6,6 +6,7 @@ $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' }
|
||||
$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 +21,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 +39,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 +133,53 @@ function Link-Default([string]$id) {
|
||||
Ok "default → $id ($dst)"
|
||||
}
|
||||
|
||||
function Set-Mirror([string]$value) {
|
||||
if ($value -notin @('auto', 'cf', 'gh', 'sf', 'ya')) {
|
||||
Die "invalid --mirror: $value (use auto|cf|gh|sf|ya)"
|
||||
}
|
||||
$script:DcrupMirror = $value
|
||||
}
|
||||
|
||||
function Get-MirrorUrl([string]$mirror, [string]$tag, [string]$asset) {
|
||||
switch ($mirror) {
|
||||
'cf' { return "https://cdn-global.dcr-tool.ru/$tag/$asset" }
|
||||
'gh' { return "https://github.com/dexoron/dcr/releases/download/$tag/$asset" }
|
||||
'sf' { return "https://sourceforge.net/projects/dcr-tool/files/$tag/$asset/download" }
|
||||
'ya' { return "https://cdn-ru.dcr-tool.ru/$tag/$asset" }
|
||||
default { Die "invalid mirror: $mirror" }
|
||||
}
|
||||
}
|
||||
|
||||
function Get-MirrorsToTry {
|
||||
switch ($script:DcrupMirror) {
|
||||
'auto' { return @('cf', 'gh', 'sf', 'ya') }
|
||||
{ $_ -in @('cf', 'gh', 'sf', 'ya') } { return @($script:DcrupMirror) }
|
||||
default { Die "invalid mirror: $($script:DcrupMirror) (use auto|cf|gh|sf|ya)" }
|
||||
}
|
||||
}
|
||||
|
||||
function Download-FromMirrors([string]$tag, [string]$asset, [string]$destFile) {
|
||||
$tmp = "$destFile.download"
|
||||
foreach ($m in (Get-MirrorsToTry)) {
|
||||
$url = Get-MirrorUrl $m $tag $asset
|
||||
Info "trying mirror $m …"
|
||||
Remove-Item $tmp -ErrorAction SilentlyContinue
|
||||
try {
|
||||
Invoke-WebRequest -Uri $url -OutFile $tmp -UserAgent 'dcrup'
|
||||
$size = (Get-Item $tmp).Length
|
||||
if ($size -gt 100000) {
|
||||
Move-Item $tmp $destFile -Force
|
||||
return $m
|
||||
}
|
||||
Warn "mirror $m returned a too-small file ($size bytes)"
|
||||
} catch {
|
||||
Warn "mirror $m failed"
|
||||
}
|
||||
Remove-Item $tmp -ErrorAction SilentlyContinue
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Fetch-Release($channel, $version) {
|
||||
if ($version) {
|
||||
return Invoke-RestMethod -Uri "https://api.github.com/repos/dexoron/dcr/releases/tags/v$version"
|
||||
@@ -142,8 +199,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 +210,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 +328,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
|
||||
|
||||
95
dcrup.sh
95
dcrup.sh
@@ -6,6 +6,7 @@ DCRUP_BIN="${DCRUP_BIN:-$DCRUP_HOME/bin}"
|
||||
DCRUP_TC="${DCRUP_HOME}/toolchains"
|
||||
DCRUP_META="${DCRUP_HOME}/meta"
|
||||
REPO_URL="${DCRUP_REPO:-https://github.com/dexoron/dcr}"
|
||||
DCRUP_MIRROR="${DCRUP_MIRROR:-auto}"
|
||||
API_LATEST="https://api.github.com/repos/dexoron/dcr/releases/latest"
|
||||
API_ALL="https://api.github.com/repos/dexoron/dcr/releases"
|
||||
FEATURES="${DCR_FEATURES:-archive}"
|
||||
@@ -26,7 +27,7 @@ usage() {
|
||||
dcrup — install and switch DCR versions
|
||||
|
||||
Usage:
|
||||
dcrup install <spec> [--build|--release] [--force] [--libc gnu|musl|auto]
|
||||
dcrup install <spec> [--build|--release] [--force] [--libc gnu|musl|auto] [--mirror auto|cf|gh|sf|ya]
|
||||
dcrup default <spec>
|
||||
dcrup update
|
||||
dcrup list
|
||||
@@ -44,7 +45,7 @@ Spec:
|
||||
night → always build from branch "dev" HEAD
|
||||
|
||||
Modes:
|
||||
--release download GitHub Release binary (default for stable/dev)
|
||||
--release download prebuilt binary (default for stable/dev)
|
||||
--build cargo build --release --features archive
|
||||
night always --build (prebuilt not available)
|
||||
|
||||
@@ -53,12 +54,21 @@ Linux libc (prebuilt triple only):
|
||||
--libc musl x86_64-unknown-linux-musl
|
||||
--libc auto musl if host ldd/libc is musl, else gnu
|
||||
|
||||
Mirrors (prebuilt download only):
|
||||
--mirror auto try cf → gh → sf → ya, first successful download wins (default)
|
||||
--mirror cf download from cdn-global.dcr-tool.ru
|
||||
--mirror gh download from github.com/dexoron/dcr
|
||||
--mirror sf download from sourceforge.net/projects/dcr-tool
|
||||
--mirror ya download from cdn-ru.dcr-tool.ru
|
||||
DCRUP_MIRROR env override (default: auto)
|
||||
|
||||
Env:
|
||||
DCRUP_HOME default ~/.dcr
|
||||
DCRUP_BIN default $DCRUP_HOME/bin
|
||||
DCR_FEATURES default archive
|
||||
DCRUP_REPO git/GitHub repo URL
|
||||
DCRUP_LIBC gnu|musl|auto (default: gnu)
|
||||
DCRUP_MIRROR auto|cf|gh|sf|ya (default: auto)
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -239,9 +249,57 @@ json_tag() {
|
||||
printf '%s' "$1" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1
|
||||
}
|
||||
|
||||
json_asset_url() {
|
||||
local json="$1" asset="$2"
|
||||
printf '%s' "$json" | sed -n "s#.*\"browser_download_url\"[[:space:]]*:[[:space:]]*\"\([^\"]*/${asset}\)\"#\1#p" | head -n1
|
||||
set_mirror() {
|
||||
case "$1" in
|
||||
auto|cf|gh|sf|ya) DCRUP_MIRROR="$1" ;;
|
||||
*) die "invalid --mirror: $1 (use auto|cf|gh|sf|ya)" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
mirror_url() {
|
||||
local mirror="$1" tag="$2" asset="$3"
|
||||
case "$mirror" in
|
||||
cf) printf 'https://cdn-global.dcr-tool.ru/%s/%s' "$tag" "$asset" ;;
|
||||
gh) printf 'https://github.com/dexoron/dcr/releases/download/%s/%s' "$tag" "$asset" ;;
|
||||
sf) printf 'https://sourceforge.net/projects/dcr-tool/files/%s/%s/download' "$tag" "$asset" ;;
|
||||
ya) printf 'https://cdn-ru.dcr-tool.ru/%s/%s' "$tag" "$asset" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
mirror_chain() { echo "cf gh sf ya"; }
|
||||
|
||||
mirrors_to_try() {
|
||||
case "$DCRUP_MIRROR" in
|
||||
auto) mirror_chain ;;
|
||||
cf|gh|sf|ya) echo "$DCRUP_MIRROR" ;;
|
||||
*) die "invalid mirror: $DCRUP_MIRROR (use auto|cf|gh|sf|ya)" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
download_from_mirrors() {
|
||||
local tag="$1" asset="$2" dest="$3"
|
||||
local tmp="$dest/dcr.download" m url size
|
||||
USED_MIRROR=""
|
||||
for m in $(mirrors_to_try); do
|
||||
url="$(mirror_url "$m" "$tag" "$asset")"
|
||||
log "trying mirror $m …"
|
||||
rm -f "$tmp"
|
||||
if curl -fL --retry 2 --retry-delay 1 --connect-timeout 10 --max-time 180 \
|
||||
-A "dcrup" "$url" -o "$tmp"; then
|
||||
size="$(wc -c <"$tmp" | tr -d ' ')"
|
||||
if [[ "${size:-0}" -gt 100000 ]]; then
|
||||
mv "$tmp" "$dest/dcr"
|
||||
USED_MIRROR="$m"
|
||||
return 0
|
||||
fi
|
||||
warn "mirror $m returned a too-small file (${size:-0} bytes)"
|
||||
else
|
||||
warn "mirror $m failed"
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
latest_stable_json() {
|
||||
@@ -281,7 +339,7 @@ release_json_for_spec() {
|
||||
}
|
||||
|
||||
install_prebuilt() {
|
||||
local triple asset tag version url json id dest
|
||||
local triple asset tag version id dest mirror
|
||||
need_cmd curl
|
||||
triple="$(detect_triple)"
|
||||
json="$(release_json_for_spec)"
|
||||
@@ -292,8 +350,6 @@ install_prebuilt() {
|
||||
case "$triple" in
|
||||
*windows*) asset="${asset}.exe" ;;
|
||||
esac
|
||||
url="$(json_asset_url "$json" "$asset")"
|
||||
[[ -n "$url" ]] || die "asset not found: $asset (tag $tag)"
|
||||
|
||||
id="$(toolchain_id "$version")"
|
||||
dest="$DCRUP_TC/$id"
|
||||
@@ -303,9 +359,12 @@ install_prebuilt() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "downloading $asset …"
|
||||
mkdir -p "$dest"
|
||||
curl -fL "$url" -o "$dest/dcr"
|
||||
log "downloading $asset (mirror=$DCRUP_MIRROR) …"
|
||||
download_from_mirrors "$tag" "$asset" "$dest" \
|
||||
|| die "no mirror reachable for $asset (try: --mirror cf|gh|sf|ya)"
|
||||
mirror="$USED_MIRROR"
|
||||
log "mirror → $mirror"
|
||||
chmod +x "$dest/dcr"
|
||||
printf '%s\n' \
|
||||
"channel=$SPEC_CHANNEL" \
|
||||
@@ -313,9 +372,10 @@ install_prebuilt() {
|
||||
"tag=$tag" \
|
||||
"source=release" \
|
||||
"triple=$triple" \
|
||||
"mirror=$mirror" \
|
||||
"libc=${DCRUP_LIBC:-gnu}" \
|
||||
>"$dest/dcrup-meta"
|
||||
ok "installed prebuilt $id ($triple)"
|
||||
ok "installed prebuilt $id ($triple via $mirror)"
|
||||
link_default "$id"
|
||||
}
|
||||
|
||||
@@ -405,6 +465,15 @@ cmd_install() {
|
||||
libc_cli="${1#--libc=}"
|
||||
shift
|
||||
;;
|
||||
--mirror)
|
||||
[[ $# -ge 2 ]] || die "--mirror needs auto|cf|gh|sf|ya"
|
||||
set_mirror "$2"
|
||||
shift 2
|
||||
;;
|
||||
--mirror=*)
|
||||
set_mirror "${1#--mirror=}"
|
||||
shift
|
||||
;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-*) die "unknown flag: $1" ;;
|
||||
*)
|
||||
@@ -414,7 +483,7 @@ cmd_install() {
|
||||
;;
|
||||
esac
|
||||
done
|
||||
[[ -n "$spec" ]] || die "usage: dcrup install <spec> [--build|--release] [--force] [--libc gnu|musl|auto]"
|
||||
[[ -n "$spec" ]] || die "usage: dcrup install <spec> [--build|--release] [--force] [--libc gnu|musl|auto] [--mirror auto|cf|gh|sf|ya]"
|
||||
parse_spec "$spec"
|
||||
ensure_dirs
|
||||
|
||||
@@ -431,7 +500,7 @@ cmd_install() {
|
||||
fi
|
||||
|
||||
export FORCE
|
||||
log "install spec=$SPEC_RAW channel=$SPEC_CHANNEL version=${SPEC_VERSION:-∅} mode=$mode libc=$DCRUP_LIBC"
|
||||
log "install spec=$SPEC_RAW channel=$SPEC_CHANNEL version=${SPEC_VERSION:-∅} mode=$mode libc=$DCRUP_LIBC mirror=$DCRUP_MIRROR"
|
||||
if [[ "$mode" == "build" ]]; then
|
||||
install_build
|
||||
else
|
||||
|
||||
@@ -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,17 @@ sidebar_label: Changelog
|
||||
|
||||
# Changelog
|
||||
|
||||
## 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`.
|
||||
|
||||
@@ -29,7 +29,7 @@ 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.
|
||||
|
||||
## `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`.
|
||||
|
||||
@@ -45,10 +45,12 @@ After `self-install`, the command is **`dcrup`** (no `.sh` / `.ps1`): shims live
|
||||
### 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
|
||||
@@ -60,6 +62,22 @@ dcrup which
|
||||
|
||||
Layout: `~/.dcr/toolchains/<id>/dcr` and `~/.dcr/bin/dcr` → active version.
|
||||
|
||||
### Download mirrors
|
||||
|
||||
Prebuilt installs try **cf → gh → sf → ya** unless you pin a host.
|
||||
|
||||
| Code | URL | Notes |
|
||||
|------|-----|--------|
|
||||
| `cf` | `https://cdn-global.dcr-tool.ru/<tag>/<asset>` | CloudFlare CDN (worldwide) |
|
||||
| `gh` | `https://github.com/dexoron/dcr/releases/download/<tag>/<asset>` | GitHub Releases |
|
||||
| `sf` | `https://sourceforge.net/projects/dcr-tool/files/<tag>/<asset>/download` | SourceForge |
|
||||
| `ya` | `https://cdn-ru.dcr-tool.ru/<tag>/<asset>` | Yandex CDN (RU fallback) |
|
||||
|
||||
```sh
|
||||
dcrup install stable --mirror auto # default
|
||||
export DCRUP_MIRROR=cf # persist for this shell
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Arch Linux (AUR)
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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,15 @@ 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"] }
|
||||
```
|
||||
|
||||
## Registry
|
||||
|
||||
DCR uses a package registry for dependency lookup by name.
|
||||
@@ -42,7 +51,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 +59,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 +84,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 +92,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 "$@"
|
||||
@@ -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!")
|
||||
@@ -288,8 +288,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, a prefixed source (path:, git:, github:, gitlab:), or a full URL",
|
||||
);
|
||||
return Err(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ use crate::core::build::steps::{
|
||||
get_build_steps_with_profile, run_build_steps, verify_expectations,
|
||||
};
|
||||
use crate::core::build_config::Config;
|
||||
use crate::core::deps::{register, resolve_deps};
|
||||
use crate::core::deps::resolve_deps;
|
||||
use crate::core::workspace::parse_workspace;
|
||||
use crate::utils::build::{
|
||||
default_target_triple, get_bool_with_profile, get_config_opt, get_config_str,
|
||||
@@ -335,6 +335,81 @@ fn build_project_at(
|
||||
workspace_root: Option<&Path>,
|
||||
cancel: &Arc<AtomicBool>,
|
||||
rep: &mut dyn BuildReporter,
|
||||
) -> Result<(), String> {
|
||||
let mut dependency_stack = Vec::new();
|
||||
build_project_at_with_stack(
|
||||
project_root,
|
||||
profile,
|
||||
target,
|
||||
explicit_target,
|
||||
exclude_dirs,
|
||||
force,
|
||||
verbose,
|
||||
workspace_root,
|
||||
cancel,
|
||||
rep,
|
||||
&mut dependency_stack,
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds a package while tracking the dependency chain to reject cycles.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build_project_at_with_stack(
|
||||
project_root: &Path,
|
||||
profile: &str,
|
||||
target: Option<&str>,
|
||||
explicit_target: bool,
|
||||
exclude_dirs: &[std::path::PathBuf],
|
||||
force: bool,
|
||||
verbose: bool,
|
||||
workspace_root: Option<&Path>,
|
||||
cancel: &Arc<AtomicBool>,
|
||||
rep: &mut dyn BuildReporter,
|
||||
dependency_stack: &mut Vec<PathBuf>,
|
||||
) -> Result<(), String> {
|
||||
let root = project_root
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| project_root.to_path_buf());
|
||||
if let Some(index) = dependency_stack.iter().position(|item| item == &root) {
|
||||
let mut chain: Vec<String> = dependency_stack[index..]
|
||||
.iter()
|
||||
.map(|item| item.display().to_string())
|
||||
.collect();
|
||||
chain.push(root.display().to_string());
|
||||
return Err(format!("Dependency cycle detected: {}", chain.join(" -> ")));
|
||||
}
|
||||
dependency_stack.push(root);
|
||||
let result = build_project_at_inner(
|
||||
project_root,
|
||||
profile,
|
||||
target,
|
||||
explicit_target,
|
||||
exclude_dirs,
|
||||
force,
|
||||
verbose,
|
||||
workspace_root,
|
||||
cancel,
|
||||
rep,
|
||||
dependency_stack,
|
||||
);
|
||||
dependency_stack.pop();
|
||||
result
|
||||
}
|
||||
|
||||
/// Builds one DCR package after the dependency-cycle guard has been installed.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build_project_at_inner(
|
||||
project_root: &Path,
|
||||
profile: &str,
|
||||
target: Option<&str>,
|
||||
explicit_target: bool,
|
||||
exclude_dirs: &[std::path::PathBuf],
|
||||
force: bool,
|
||||
verbose: bool,
|
||||
workspace_root: Option<&Path>,
|
||||
cancel: &Arc<AtomicBool>,
|
||||
rep: &mut dyn BuildReporter,
|
||||
dependency_stack: &mut Vec<PathBuf>,
|
||||
) -> Result<(), String> {
|
||||
if cancel.load(Ordering::SeqCst) {
|
||||
return Err("Build interrupted".to_string());
|
||||
@@ -641,62 +716,57 @@ 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,
|
||||
has_explicit_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) =
|
||||
@@ -919,6 +989,40 @@ 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);
|
||||
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 {
|
||||
|
||||
@@ -4,4 +4,5 @@ pub struct ResolvedDeps {
|
||||
pub include_dirs: Vec<String>,
|
||||
pub lib_dirs: Vec<String>,
|
||||
pub libs: Vec<String>,
|
||||
pub package_roots: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ fn dep_version(path: &Path) -> String {
|
||||
|
||||
/// Resolves dependencies from config into include/lib dirs and lib names, and writes the lock file.
|
||||
///
|
||||
/// Registry and path deps contribute to `ResolvedDeps`. Git deps are recorded in the lock only
|
||||
/// (no fetch or include/lib resolution).
|
||||
/// Registry, path, and git deps contribute to `ResolvedDeps`. Path and git sources support both
|
||||
/// DCR packages (built from `dcr.toml`) and prebuilt libraries (described with include/lib/libs).
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `config`: Loaded `dcr.toml`.
|
||||
@@ -88,6 +88,9 @@ pub fn resolve_deps(
|
||||
.to_string(),
|
||||
);
|
||||
resolved.libs.push(name.clone());
|
||||
resolved
|
||||
.package_roots
|
||||
.push(dep_root.to_string_lossy().to_string());
|
||||
|
||||
lock_packages.push(DepLock {
|
||||
name: name.clone(),
|
||||
@@ -107,6 +110,8 @@ pub fn resolve_deps(
|
||||
});
|
||||
} else if let Some(path) = path_dep_path(value) {
|
||||
let dep_root = project_root.join(path);
|
||||
let dcr_package_name = package_name(&dep_root);
|
||||
let use_dcr_package = use_dcr_package(value, dcr_package_name.is_some(), name)?;
|
||||
if let Some(table) = value.as_table() {
|
||||
if let Some(includes) = table.get("include").and_then(|v| v.as_array()) {
|
||||
for inc in includes {
|
||||
@@ -118,10 +123,7 @@ pub fn resolve_deps(
|
||||
}
|
||||
} else {
|
||||
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
|
||||
push_if_exists(
|
||||
&mut resolved.include_dirs,
|
||||
&dep_root.join("target").join("include"),
|
||||
);
|
||||
push_package_output_dirs(&mut resolved, &dep_root);
|
||||
}
|
||||
|
||||
if let Some(lib_dirs) = table.get("lib").and_then(|v| v.as_array()) {
|
||||
@@ -143,16 +145,29 @@ pub fn resolve_deps(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resolved.libs.push(name.clone());
|
||||
resolved
|
||||
.libs
|
||||
.push(dcr_package_name.clone().unwrap_or_else(|| name.clone()));
|
||||
}
|
||||
} else {
|
||||
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
|
||||
push_if_exists(
|
||||
&mut resolved.include_dirs,
|
||||
&dep_root.join("target").join("include"),
|
||||
);
|
||||
push_package_output_dirs(&mut resolved, &dep_root);
|
||||
push_default_lib_dirs(&mut resolved.lib_dirs, &dep_root);
|
||||
resolved.libs.push(name.clone());
|
||||
resolved
|
||||
.libs
|
||||
.push(dcr_package_name.clone().unwrap_or_else(|| name.clone()));
|
||||
}
|
||||
|
||||
if use_dcr_package {
|
||||
resolved
|
||||
.package_roots
|
||||
.push(dep_root.to_string_lossy().to_string());
|
||||
push_package_output_dirs(&mut resolved, &dep_root);
|
||||
} else if !has_complete_prebuilt_layout(value) {
|
||||
return Err(format!(
|
||||
"Path dependency `{}` has no dcr.toml; specify include, lib, and libs for a prebuilt library",
|
||||
name
|
||||
));
|
||||
}
|
||||
|
||||
lock_packages.push(DepLock {
|
||||
@@ -162,11 +177,46 @@ pub fn resolve_deps(
|
||||
source: format!("path+{}", dep_root.display()),
|
||||
});
|
||||
} else if let Some(git_info) = git_dep(value) {
|
||||
let dep_root = register::fetch_git_dependency(
|
||||
git_info.url,
|
||||
git_info.branch.as_deref(),
|
||||
git_info.tag.as_deref(),
|
||||
git_info.rev.as_deref(),
|
||||
)?;
|
||||
let dcr_package_name = package_name(&dep_root);
|
||||
let use_dcr_package = use_dcr_package(value, dcr_package_name.is_some(), name)?;
|
||||
let (has_include, has_lib, has_libs) = value
|
||||
.as_table()
|
||||
.map(|table| push_explicit_layout(&mut resolved, &dep_root, table))
|
||||
.unwrap_or((false, false, false));
|
||||
if !has_include {
|
||||
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
|
||||
if use_dcr_package {
|
||||
push_package_output_dirs(&mut resolved, &dep_root);
|
||||
}
|
||||
}
|
||||
if !has_lib {
|
||||
push_default_lib_dirs(&mut resolved.lib_dirs, &dep_root);
|
||||
}
|
||||
if !has_libs && let Some(package_name) = dcr_package_name.as_deref() {
|
||||
resolved.libs.push(package_name.to_string());
|
||||
}
|
||||
if use_dcr_package {
|
||||
resolved
|
||||
.package_roots
|
||||
.push(dep_root.to_string_lossy().to_string());
|
||||
} else if !(has_include && has_lib && has_libs) {
|
||||
return Err(format!(
|
||||
"Git dependency `{}` has no dcr.toml; specify include, lib, and libs for a prebuilt library",
|
||||
name
|
||||
));
|
||||
}
|
||||
let commit = register::git_commit(&dep_root)?;
|
||||
lock_packages.push(DepLock {
|
||||
name: name.clone(),
|
||||
version: git_info.version.unwrap_or_default(),
|
||||
checksum: String::new(),
|
||||
source: format!("git+{}", git_info.url),
|
||||
version: git_info.version.unwrap_or(commit.clone()),
|
||||
checksum: commit.clone(),
|
||||
source: format!("git+{}#{}", git_info.url, commit),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -187,6 +237,9 @@ pub fn resolve_deps(
|
||||
struct GitDep<'a> {
|
||||
url: &'a str,
|
||||
version: Option<String>,
|
||||
branch: Option<String>,
|
||||
tag: Option<String>,
|
||||
rev: Option<String>,
|
||||
}
|
||||
|
||||
/// Parses a git dependency from a TOML value table.
|
||||
@@ -198,7 +251,25 @@ 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
|
||||
}
|
||||
@@ -226,6 +297,117 @@ fn push_default_lib_dirs(paths: &mut Vec<String>, dep_root: &Path) {
|
||||
push_if_exists(paths, &dep_root.join("target").join("lib"));
|
||||
}
|
||||
|
||||
/// Adds explicitly configured prebuilt include/lib directories and linker names.
|
||||
fn push_explicit_layout(
|
||||
resolved: &mut ResolvedDeps,
|
||||
dep_root: &Path,
|
||||
table: &toml::map::Map<String, toml::Value>,
|
||||
) -> (bool, bool, bool) {
|
||||
let mut has_include = false;
|
||||
let mut has_lib = false;
|
||||
let mut has_libs = false;
|
||||
if let Some(includes) = table.get("include").and_then(|v| v.as_array()) {
|
||||
has_include = true;
|
||||
for inc in includes {
|
||||
if let Some(path) = inc.as_str() {
|
||||
resolved
|
||||
.include_dirs
|
||||
.push(dep_root.join(path).to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(lib_dirs) = table.get("lib").and_then(|v| v.as_array()) {
|
||||
has_lib = true;
|
||||
for path in lib_dirs {
|
||||
if let Some(path) = path.as_str() {
|
||||
resolved
|
||||
.lib_dirs
|
||||
.push(dep_root.join(path).to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(libs) = table.get("libs").and_then(|v| v.as_array()) {
|
||||
has_libs = true;
|
||||
for lib in libs {
|
||||
if let Some(name) = lib.as_str() {
|
||||
resolved.libs.push(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
(has_include, has_lib, has_libs)
|
||||
}
|
||||
|
||||
/// Reports whether a source without a DCR manifest fully describes its prebuilt layout.
|
||||
fn has_complete_prebuilt_layout(value: &toml::Value) -> bool {
|
||||
value.as_table().is_some_and(|table| {
|
||||
["include", "lib", "libs"].iter().all(|field| {
|
||||
table
|
||||
.get(*field)
|
||||
.and_then(|value| value.as_array())
|
||||
.is_some_and(|values| !values.is_empty())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Selects automatic, forced-build, or forced-prebuilt dependency resolution.
|
||||
fn use_dcr_package(
|
||||
value: &toml::Value,
|
||||
has_manifest: bool,
|
||||
dependency_name: &str,
|
||||
) -> Result<bool, String> {
|
||||
let mode = value
|
||||
.as_table()
|
||||
.and_then(|table| table.get("mode"))
|
||||
.map(|value| {
|
||||
value.as_str().ok_or_else(|| {
|
||||
format!(
|
||||
"Dependency `{dependency_name}` field `mode` must be \"build\" or \"prebuild\""
|
||||
)
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
match mode {
|
||||
None => Ok(has_manifest),
|
||||
Some("build") if has_manifest => Ok(true),
|
||||
Some("build") => Err(format!(
|
||||
"Dependency `{dependency_name}` uses mode = \"build\" but has no dcr.toml"
|
||||
)),
|
||||
Some("prebuild") => Ok(false),
|
||||
Some(_) => Err(format!(
|
||||
"Dependency `{dependency_name}` field `mode` must be \"build\" or \"prebuild\""
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds DCR's packaged output directories even before the dependency is built.
|
||||
fn push_package_output_dirs(resolved: &mut ResolvedDeps, dep_root: &Path) {
|
||||
push_unique_path(
|
||||
&mut resolved.include_dirs,
|
||||
dep_root.join("target").join("include"),
|
||||
);
|
||||
push_unique_path(&mut resolved.lib_dirs, dep_root.join("target").join("lib"));
|
||||
}
|
||||
|
||||
/// Adds a path once, preserving the linker and include-search order.
|
||||
fn push_unique_path(paths: &mut Vec<String>, path: std::path::PathBuf) {
|
||||
let path = path.to_string_lossy().to_string();
|
||||
if !paths.contains(&path) {
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads a DCR package name when the dependency has a manifest.
|
||||
fn package_name(path: &Path) -> Option<String> {
|
||||
Config::open(&path.join("dcr.toml").to_string_lossy())
|
||||
.ok()
|
||||
.and_then(|config| {
|
||||
config
|
||||
.get("package.name")
|
||||
.and_then(|value| value.as_str())
|
||||
.map(str::to_string)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
/// Represents a registry configuration entry with its URL and priority.
|
||||
@@ -149,6 +151,95 @@ pub fn registry_lib_dir(dep_root: &Path) -> PathBuf {
|
||||
dep_root.join("target").join("lib")
|
||||
}
|
||||
|
||||
/// Fetches a git dependency into the DCR cache and returns its working tree.
|
||||
/// The URL and selected ref determine the cache key; an existing checkout is reused.
|
||||
pub fn fetch_git_dependency(
|
||||
url: &str,
|
||||
branch: Option<&str>,
|
||||
tag: Option<&str>,
|
||||
rev: Option<&str>,
|
||||
) -> Result<PathBuf, String> {
|
||||
if !crate::utils::git::is_git_available() {
|
||||
return Err("git executable not found in PATH".to_string());
|
||||
}
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(url.as_bytes());
|
||||
hasher.update(b"\0");
|
||||
hasher.update(branch.unwrap_or("").as_bytes());
|
||||
hasher.update(b"\0");
|
||||
hasher.update(tag.unwrap_or("").as_bytes());
|
||||
hasher.update(b"\0");
|
||||
hasher.update(rev.unwrap_or("").as_bytes());
|
||||
let key = format!("{:x}", hasher.finalize());
|
||||
let root = dcr_config_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join("git")
|
||||
.join(&key);
|
||||
if root.join(".git").is_dir() {
|
||||
let status = Command::new("git")
|
||||
.args(["fetch", "--all", "--tags", "--prune"])
|
||||
.current_dir(&root)
|
||||
.status()
|
||||
.map_err(|e| format!("failed to execute git fetch: {e}"))?;
|
||||
if !status.success() {
|
||||
eprintln!("Warning: git fetch failed for {url}; using the cached checkout");
|
||||
}
|
||||
return checkout_git_ref(&root, branch, tag, rev);
|
||||
}
|
||||
if root.exists() {
|
||||
return Err(format!(
|
||||
"git cache path is not a repository: {}",
|
||||
root.display()
|
||||
));
|
||||
}
|
||||
if let Some(parent) = root.parent() {
|
||||
fs::create_dir_all(parent).map_err(|e| format!("failed to create git cache: {e}"))?;
|
||||
}
|
||||
let status = Command::new("git")
|
||||
.args(["clone", "--", url])
|
||||
.arg(&root)
|
||||
.status()
|
||||
.map_err(|e| format!("failed to execute git clone: {e}"))?;
|
||||
if !status.success() {
|
||||
return Err(format!("git clone failed for {url}"));
|
||||
}
|
||||
checkout_git_ref(&root, branch, tag, rev)
|
||||
}
|
||||
|
||||
/// Returns the exact commit checked out in a cached git dependency.
|
||||
pub fn git_commit(root: &Path) -> Result<String, String> {
|
||||
let output = Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.current_dir(root)
|
||||
.output()
|
||||
.map_err(|e| format!("failed to execute git rev-parse: {e}"))?;
|
||||
if !output.status.success() {
|
||||
return Err(format!("failed to read git revision at {}", root.display()));
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
}
|
||||
|
||||
fn checkout_git_ref(
|
||||
root: &Path,
|
||||
branch: Option<&str>,
|
||||
tag: Option<&str>,
|
||||
rev: Option<&str>,
|
||||
) -> Result<PathBuf, String> {
|
||||
let selected = rev.or(tag).or(branch);
|
||||
let Some(reference) = selected else {
|
||||
return Ok(root.to_path_buf());
|
||||
};
|
||||
let status = Command::new("git")
|
||||
.args(["checkout", "--force", reference])
|
||||
.current_dir(root)
|
||||
.status()
|
||||
.map_err(|e| format!("failed to execute git checkout: {e}"))?;
|
||||
if !status.success() {
|
||||
return Err(format!("git checkout failed for ref `{reference}`"));
|
||||
}
|
||||
Ok(root.to_path_buf())
|
||||
}
|
||||
|
||||
/// Looks up `name` in the local package index (`DCR_INDEX_PATH` / `~/.dcr/index.json`).
|
||||
///
|
||||
/// Configured registries are sorted by priority, but the lookup currently always reads the
|
||||
|
||||
@@ -16,6 +16,15 @@ fn dcr_add_dependencies() {
|
||||
"path dep not found in toml"
|
||||
);
|
||||
|
||||
// A bare filesystem path is equivalent to the explicit path: form.
|
||||
let out = run_dcr(&["add", "otherlib", "../otherlib"], &dir);
|
||||
assert!(out.status.success(), "dcr add bare path should succeed");
|
||||
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
|
||||
assert!(
|
||||
toml.contains("otherlib = { path = \"../otherlib\" }"),
|
||||
"bare path dep not found in toml"
|
||||
);
|
||||
|
||||
// Test github: prefix
|
||||
let out = run_dcr(&["add", "gh_lib", "github:user/repo"], &dir);
|
||||
assert!(out.status.success(), "dcr add github should succeed");
|
||||
@@ -96,6 +105,369 @@ fn dcr_builds_lib_package() {
|
||||
assert!(target_dir.join("lib").exists(), "lib directory missing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_prebuilt_dependency_without_manifest_links() {
|
||||
let Some(compiler) = available_compiler() else {
|
||||
eprintln!("no compiler found; skipping prebuilt path dependency test");
|
||||
return;
|
||||
};
|
||||
if !is_ar_in_path() {
|
||||
eprintln!("ar unavailable; skipping prebuilt path dependency test");
|
||||
return;
|
||||
}
|
||||
|
||||
let root = unique_sandbox_dir("path_prebuilt_dep");
|
||||
let dep = root.join("dep");
|
||||
let app = root.join("app");
|
||||
write_prebuilt_library(
|
||||
&dep,
|
||||
compiler,
|
||||
"prebuilt",
|
||||
"int prebuilt_answer(void);\n",
|
||||
"int prebuilt_answer(void) { return 42; }\n",
|
||||
);
|
||||
std::fs::create_dir_all(app.join("src")).unwrap();
|
||||
std::fs::write(
|
||||
app.join("dcr.toml"),
|
||||
"[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\nprebuilt = { path = \"../dep\", include = [\"include\"], lib = [\"lib\"], libs = [\"prebuilt\"] }\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
app.join("src/main.c"),
|
||||
"#include \"prebuilt.h\"\nint main(void) { return prebuilt_answer() == 42 ? 0 : 1; }\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let out = run_dcr_env(&["build"], &app, &[("DCR_COMPILER", compiler)]);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"prebuilt path dependency build failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_dcr_dependency_is_built_before_consumer() {
|
||||
let Some(compiler) = available_compiler() else {
|
||||
eprintln!("no compiler found; skipping path dependency build test");
|
||||
return;
|
||||
};
|
||||
let root = unique_sandbox_dir("path_dcr_dep");
|
||||
let dep = root.join("dep");
|
||||
let app = root.join("app");
|
||||
std::fs::create_dir_all(dep.join("src")).unwrap();
|
||||
std::fs::create_dir_all(app.join("src")).unwrap();
|
||||
std::fs::write(
|
||||
dep.join("dcr.toml"),
|
||||
"[package]\nname = \"mathlib\"\nversion = \"0.1.0\"\ntype = \"lib\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"staticlib\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(dep.join("src/mathlib.h"), "int answer(void);\n").unwrap();
|
||||
std::fs::write(
|
||||
dep.join("src/mathlib.c"),
|
||||
"int answer(void) { return 42; }\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
app.join("dcr.toml"),
|
||||
"[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\nrenamed = { path = \"../dep\" }\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
app.join("src/main.c"),
|
||||
"#include \"mathlib.h\"\nint main(void) { return answer() == 42 ? 0 : 1; }\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let out = run_dcr_env(&["build"], &app, &[("DCR_COMPILER", compiler)]);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"path dependency build failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(dep.join("target/include/mathlib.h").is_file());
|
||||
assert!(dep.join("target/lib").is_dir());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_dcr_dependencies_build_transitively() {
|
||||
let Some(compiler) = available_compiler() else {
|
||||
eprintln!("no compiler found; skipping transitive path dependency test");
|
||||
return;
|
||||
};
|
||||
let root = unique_sandbox_dir("path_dcr_transitive");
|
||||
let leaf = root.join("leaf");
|
||||
let middle = root.join("middle");
|
||||
let app = root.join("app");
|
||||
for project in [&leaf, &middle, &app] {
|
||||
std::fs::create_dir_all(project.join("src")).unwrap();
|
||||
}
|
||||
std::fs::write(
|
||||
leaf.join("dcr.toml"),
|
||||
"[package]\nname = \"leaf\"\nversion = \"0.1.0\"\ntype = \"lib\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"staticlib\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(leaf.join("src/leaf.h"), "int leaf_value(void);\n").unwrap();
|
||||
std::fs::write(
|
||||
leaf.join("src/leaf.c"),
|
||||
"int leaf_value(void) { return 40; }\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
middle.join("dcr.toml"),
|
||||
"[package]\nname = \"middle\"\nversion = \"0.1.0\"\ntype = \"lib\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"staticlib\"\n\n[dependencies]\nleaf = { path = \"../leaf\" }\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(middle.join("src/middle.h"), "int middle_value(void);\n").unwrap();
|
||||
std::fs::write(
|
||||
middle.join("src/middle.c"),
|
||||
"#include \"leaf.h\"\nint middle_value(void) { return leaf_value() + 2; }\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
app.join("dcr.toml"),
|
||||
"[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\nmiddle = { path = \"../middle\" }\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
app.join("src/main.c"),
|
||||
"#include \"middle.h\"\nint main(void) { return middle_value() == 42 ? 0 : 1; }\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let out = run_dcr_env(&["build"], &app, &[("DCR_COMPILER", compiler)]);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"transitive path dependency build failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(leaf.join("target/lib").is_dir());
|
||||
assert!(middle.join("target/lib").is_dir());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_dependency_cycle_reports_the_full_chain() {
|
||||
let root = unique_sandbox_dir("path_dcr_cycle");
|
||||
let first = root.join("first");
|
||||
let second = root.join("second");
|
||||
for project in [&first, &second] {
|
||||
std::fs::create_dir_all(project.join("src")).unwrap();
|
||||
}
|
||||
std::fs::write(
|
||||
first.join("dcr.toml"),
|
||||
"[package]\nname = \"first\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nkind = \"staticlib\"\n\n[dependencies]\nsecond = { path = \"../second\" }\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
second.join("dcr.toml"),
|
||||
"[package]\nname = \"second\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nkind = \"staticlib\"\n\n[dependencies]\nfirst = { path = \"../first\" }\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let out = run_dcr(&["build"], &first);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(!out.status.success());
|
||||
assert!(stderr.contains("Dependency cycle detected"), "{stderr}");
|
||||
assert!(
|
||||
stderr.contains("first") && stderr.contains("second"),
|
||||
"{stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_dcr_dependency_is_cloned_built_and_locked() {
|
||||
let Some(compiler) = available_compiler() else {
|
||||
eprintln!("no compiler found; skipping git dependency build test");
|
||||
return;
|
||||
};
|
||||
if !is_git_in_path() {
|
||||
eprintln!("git unavailable; skipping git dependency build test");
|
||||
return;
|
||||
}
|
||||
let root = unique_sandbox_dir("git_dcr_dep");
|
||||
let home = root.join("home");
|
||||
let origin = root.join("origin");
|
||||
let app = root.join("app");
|
||||
std::fs::create_dir_all(origin.join("src")).unwrap();
|
||||
std::fs::create_dir_all(app.join("src")).unwrap();
|
||||
std::fs::write(
|
||||
origin.join("dcr.toml"),
|
||||
"[package]\nname = \"gitmath\"\nversion = \"0.1.0\"\ntype = \"lib\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"staticlib\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(origin.join("src/gitmath.h"), "int git_answer(void);\n").unwrap();
|
||||
std::fs::write(
|
||||
origin.join("src/gitmath.c"),
|
||||
"int git_answer(void) { return 7; }\n",
|
||||
)
|
||||
.unwrap();
|
||||
for args in [
|
||||
vec!["init"],
|
||||
vec!["add", "."],
|
||||
vec![
|
||||
"-c",
|
||||
"user.email=dcr@example.test",
|
||||
"-c",
|
||||
"user.name=DCR",
|
||||
"commit",
|
||||
"-m",
|
||||
"init",
|
||||
],
|
||||
vec!["tag", "v0.1.0"],
|
||||
] {
|
||||
let status = std::process::Command::new("git")
|
||||
.args(args)
|
||||
.current_dir(&origin)
|
||||
.status()
|
||||
.unwrap();
|
||||
assert!(status.success());
|
||||
}
|
||||
// TOML basic strings treat backslashes as escapes. Git for Windows emits
|
||||
// paths with `\\`, so normalize the local repository path before embedding
|
||||
// it into dcr.toml (the slash form is accepted by Git on every platform).
|
||||
let origin_url = origin.to_string_lossy().replace('\\', "/");
|
||||
std::fs::write(
|
||||
app.join("dcr.toml"),
|
||||
format!("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\ngitmath = {{ git = \"{origin_url}\", tag = \"v0.1.0\" }}\n"),
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
app.join("src/main.c"),
|
||||
"#include \"gitmath.h\"\nint main(void) { return git_answer() == 7 ? 0 : 1; }\n",
|
||||
)
|
||||
.unwrap();
|
||||
let home_s = home.to_string_lossy().to_string();
|
||||
let out = run_dcr_env(
|
||||
&["build"],
|
||||
&app,
|
||||
&[("DCR_COMPILER", compiler), ("HOME", home_s.as_str())],
|
||||
);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"git dependency build failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
let lock = std::fs::read_to_string(app.join("dcr.lock")).unwrap();
|
||||
assert!(lock.contains("git+"));
|
||||
assert!(!lock.contains("checksum = \"\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_prebuilt_dependency_without_manifest_links() {
|
||||
let Some(compiler) = available_compiler() else {
|
||||
eprintln!("no compiler found; skipping prebuilt git dependency test");
|
||||
return;
|
||||
};
|
||||
if !is_git_in_path() || !is_ar_in_path() {
|
||||
eprintln!("git or ar unavailable; skipping prebuilt git dependency test");
|
||||
return;
|
||||
}
|
||||
|
||||
let root = unique_sandbox_dir("git_prebuilt_dep");
|
||||
let home = root.join("home");
|
||||
let origin = root.join("origin");
|
||||
let app = root.join("app");
|
||||
write_prebuilt_library(
|
||||
&origin,
|
||||
compiler,
|
||||
"gitprebuilt",
|
||||
"int git_prebuilt_answer(void);\n",
|
||||
"int git_prebuilt_answer(void) { return 9; }\n",
|
||||
);
|
||||
std::fs::create_dir_all(app.join("src")).unwrap();
|
||||
for args in [
|
||||
vec!["init"],
|
||||
vec!["add", "."],
|
||||
vec![
|
||||
"-c",
|
||||
"user.email=dcr@example.test",
|
||||
"-c",
|
||||
"user.name=DCR",
|
||||
"commit",
|
||||
"-m",
|
||||
"init",
|
||||
],
|
||||
vec!["tag", "v0.1.0"],
|
||||
] {
|
||||
let status = std::process::Command::new("git")
|
||||
.args(args)
|
||||
.current_dir(&origin)
|
||||
.status()
|
||||
.unwrap();
|
||||
assert!(status.success());
|
||||
}
|
||||
// Keep the Git source TOML-safe on Windows (`C:/...`, not `C:\\...`).
|
||||
let origin_url = origin.to_string_lossy().replace('\\', "/");
|
||||
std::fs::write(
|
||||
app.join("dcr.toml"),
|
||||
format!("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\ngitprebuilt = {{ git = \"{origin_url}\", tag = \"v0.1.0\", include = [\"include\"], lib = [\"lib\"], libs = [\"gitprebuilt\"] }}\n"),
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
app.join("src/main.c"),
|
||||
"#include \"gitprebuilt.h\"\nint main(void) { return git_prebuilt_answer() == 9 ? 0 : 1; }\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let home_s = home.to_string_lossy().to_string();
|
||||
let out = run_dcr_env(
|
||||
&["build"],
|
||||
&app,
|
||||
&[("DCR_COMPILER", compiler), ("HOME", home_s.as_str())],
|
||||
);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"prebuilt git dependency build failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
let lock = std::fs::read_to_string(app.join("dcr.lock")).unwrap();
|
||||
assert!(lock.contains("git+"));
|
||||
assert!(!lock.contains("checksum = \"\""));
|
||||
}
|
||||
|
||||
fn write_prebuilt_library(
|
||||
root: &std::path::Path,
|
||||
compiler: &str,
|
||||
name: &str,
|
||||
header: &str,
|
||||
source: &str,
|
||||
) {
|
||||
let include_dir = root.join("include");
|
||||
let lib_dir = root.join("lib");
|
||||
let src_dir = root.join("src");
|
||||
std::fs::create_dir_all(&include_dir).unwrap();
|
||||
std::fs::create_dir_all(&lib_dir).unwrap();
|
||||
std::fs::create_dir_all(&src_dir).unwrap();
|
||||
std::fs::write(include_dir.join(format!("{name}.h")), header).unwrap();
|
||||
let source_path = src_dir.join(format!("{name}.c"));
|
||||
let object_path = lib_dir.join(format!("{name}.o"));
|
||||
let archive_path = lib_dir.join(format!("lib{name}.a"));
|
||||
std::fs::write(&source_path, source).unwrap();
|
||||
let status = std::process::Command::new(compiler)
|
||||
.args(["-c"])
|
||||
.arg(&source_path)
|
||||
.args(["-o"])
|
||||
.arg(&object_path)
|
||||
.status()
|
||||
.unwrap();
|
||||
assert!(status.success(), "failed to compile prebuilt library");
|
||||
let status = std::process::Command::new("ar")
|
||||
.args(["rcs"])
|
||||
.arg(&archive_path)
|
||||
.arg(&object_path)
|
||||
.status()
|
||||
.unwrap();
|
||||
assert!(status.success(), "failed to archive prebuilt library");
|
||||
}
|
||||
|
||||
fn is_ar_in_path() -> bool {
|
||||
std::process::Command::new("ar")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|out| out.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_dependency_is_built_from_cache() {
|
||||
let Some(compiler) = available_compiler() else {
|
||||
|
||||
Reference in New Issue
Block a user