8 Commits

Author SHA1 Message Date
1a6a261cac fix(ci): fix tests 2026-07-23 21:35:59 +08:00
f92cc6ae91 fix(ci): fix tests 2026-07-23 19:59:37 +08:00
5aefbc6498 fix(ci): fix tests 2026-07-23 19:53:48 +08:00
594007f022 fix(ci): fix tests 2026-07-23 19:45:31 +08:00
d10ec84004 fix(ci): fix tests 2026-07-23 19:30:39 +08:00
574ba8b6d0 fix(ci): fix tests 2026-07-23 16:11:39 +08:00
75eace9d73 feat: add dcrup (sh/ps1/cmd) and fix cross-platform unit tests 2026-07-23 14:59:28 +08:00
2446cb7970 feat(docs): remove DPM in install steps 2026-07-23 14:03:08 +08:00
19 changed files with 1485 additions and 215 deletions

View File

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

View File

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

13
dcrup.cmd Executable file
View File

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

367
dcrup.ps1 Normal file
View File

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

599
dcrup.sh Executable file
View File

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

View File

@@ -8,8 +8,10 @@ sidebar_label: Introduction
<summary>Quick reference</summary>
```bash
# Installation
curl -fsSL https://dcr.dexoron.su/install.sh | sh
# Installation (dcrup)
curl -fsSL https://sh.dcr-tool.ru | sh -s -- self-install
export PATH="$HOME/.dcr/bin:$PATH"
dcrup install stable
# Create a project
dcr new my-app

View File

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

View File

@@ -125,17 +125,27 @@ fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
let target = flags
.target
.clone()
.or_else(|| {
config
.get("build.target")
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
})
.or_else(|| Some(default_target_triple()));
let target = flags.target.clone().or_else(|| {
config
.get("build.target")
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
});
let target = match target {
Some(t) => Some(crate::utils::build::normalize_target_os(&t).to_string()),
None if cfg!(target_os = "linux")
|| cfg!(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)) =>
{
Some(default_target_triple())
}
None => None,
};
if flags.all
&& let Some(workspace) = parse_workspace(

View File

@@ -160,14 +160,12 @@ pub(crate) fn archive_static(
return Ok(common::elapsed_secs(start_time));
}
let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") {
"lib"
} else {
"ar"
});
let archiver = ctx
.archiver
.unwrap_or_else(|| default_archiver(ctx.compiler));
let mut cmd = Command::new(archiver);
if cfg!(target_os = "windows") && archiver == "lib" {
if cfg!(target_os = "windows") && is_msvc_lib(archiver) {
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
} else {
cmd.arg("rcs").arg(&lib_path);
@@ -243,6 +241,26 @@ pub(crate) fn link_binary(
run(ctx, cmd, start_time)
}
fn default_archiver(compiler: &str) -> &'static str {
let c = compiler.to_lowercase();
if cfg!(target_os = "windows") {
if c.contains("cl") && !c.contains("clang") {
"lib"
} else if c.contains("clang") {
"llvm-ar"
} else {
"ar"
}
} else {
"ar"
}
}
fn is_msvc_lib(archiver: &str) -> bool {
let a = archiver.to_lowercase();
a == "lib" || a == "lib.exe" || a.ends_with("\\lib.exe") || a.ends_with("/lib.exe")
}
fn run(ctx: &BuildContext, mut cmd: Command, start_time: Instant) -> Result<f64, String> {
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);

View File

@@ -60,8 +60,13 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let elapsed = common::elapsed_secs(start_time);
return Ok(elapsed);
}
let mut cmd = Command::new(ctx.archiver.unwrap_or("lib"));
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
let archiver = ctx.archiver.unwrap_or("lib");
let mut cmd = Command::new(archiver);
if archiver == "lib" || archiver.eq_ignore_ascii_case("lib.exe") {
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
} else {
cmd.arg("rcs").arg(&lib_path);
}
for obj in &objects {
cmd.arg(obj);
}

View File

@@ -158,7 +158,26 @@ pub fn is_excluded(path: &Path, exclude_dirs: &[PathBuf], include_paths: &[Strin
if matches_patterns(path, include_paths, false) {
return true;
}
exclude_dirs.iter().any(|dir| path.starts_with(dir))
exclude_dirs.iter().any(|dir| path_is_under(path, dir))
}
fn path_is_under(path: &Path, prefix: &Path) -> bool {
if path.starts_with(prefix) {
return true;
}
match (fs::canonicalize(path), fs::canonicalize(prefix)) {
(Ok(p), Ok(pref)) => p.starts_with(pref),
_ => {
let p = path.to_string_lossy().replace('\\', "/");
let pref = prefix.to_string_lossy().replace('\\', "/");
if p == pref || p.starts_with(&format!("{pref}/")) {
return true;
}
let p = p.strip_prefix("/private").unwrap_or(&p);
let pref = pref.strip_prefix("/private").unwrap_or(&pref);
p == pref || p.starts_with(&format!("{pref}/"))
}
}
}
fn matches_patterns(path: &Path, patterns: &[String], positive: bool) -> bool {
@@ -379,7 +398,7 @@ pub fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
} else {
out.set_extension(obj_ext);
}
out.to_string_lossy().to_string()
out.to_string_lossy().replace('\\', "/")
}
pub fn needs_rebuild(source: &str, object: &str) -> bool {
@@ -482,7 +501,11 @@ fn parse_d_file(content: &str) -> Vec<String> {
}
in_escape = false;
} else if c == '\\' {
in_escape = true;
match chars.peek() {
Some('\n' | '\r' | ' ' | '\t' | '\\') => in_escape = true,
Some(_) => current_path.push('\\'),
None => current_path.push('\\'),
}
} else if c.is_whitespace() {
if !current_path.is_empty() {
deps.push(current_path.clone());
@@ -509,12 +532,18 @@ mod tests {
static TEST_COUNTER: AtomicUsize = AtomicUsize::new(0);
static CWD_LOCK: Mutex<()> = Mutex::new(());
fn cwd_lock() -> std::sync::MutexGuard<'static, ()> {
CWD_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn temp_dir(prefix: &str) -> PathBuf {
let n = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let dir = std::env::temp_dir().join(format!("dcr_test_{prefix}_{n}"));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
dir
fs::canonicalize(&dir).unwrap_or(dir)
}
fn default_roots() -> Vec<PathBuf> {
@@ -679,7 +708,7 @@ mod tests {
fs::write(src.join("utils.c"), "").unwrap();
fs::write(src.join("README.md"), "").unwrap(); // should be ignored
let _guard = CWD_LOCK.lock().unwrap();
let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new();
@@ -703,7 +732,7 @@ mod tests {
fs::write(src.join("other.cc"), "").unwrap();
fs::write(src.join("skip.c"), "").unwrap(); // should be ignored
let _guard = CWD_LOCK.lock().unwrap();
let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new();
@@ -721,7 +750,7 @@ mod tests {
let src = dir.join("src");
fs::create_dir_all(&src).unwrap();
let _guard = CWD_LOCK.lock().unwrap();
let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new();
@@ -743,7 +772,7 @@ mod tests {
fs::write(vendor.join("lib.c"), "").unwrap(); // should be excluded
let exclude = vec![vendor.clone()];
let _guard = CWD_LOCK.lock().unwrap();
let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new();
@@ -765,7 +794,7 @@ mod tests {
fs::write(src.join("main.c"), "").unwrap();
fs::write(sub.join("nested.c"), "").unwrap();
let _guard = CWD_LOCK.lock().unwrap();
let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new();
@@ -789,7 +818,7 @@ mod tests {
let exclude = vec![boot.clone()];
let include = vec!["src/boot/arch/**".to_string()];
let _guard = CWD_LOCK.lock().unwrap();
let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let roots = default_roots();
@@ -816,7 +845,7 @@ mod tests {
"!src/legacy/**".to_string(),
"src/legacy/allow/**".to_string(),
];
let _guard = CWD_LOCK.lock().unwrap();
let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let roots = default_roots();
@@ -839,7 +868,7 @@ mod tests {
let exclude: Vec<PathBuf> = Vec::new();
let include = vec!["!src/gen/**".to_string()];
let _guard = CWD_LOCK.lock().unwrap();
let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let roots = default_roots();
@@ -874,36 +903,44 @@ mod tests {
#[test]
fn needs_rebuild_header_modified() {
let dir = temp_dir("rebuild_header");
let src = dir.join("test.c");
let header = dir.join("test.h");
let obj = dir.join("test.o");
let d_file = dir.join("test.d");
fs::write(dir.join("test.c"), "int main() {}").unwrap();
fs::write(dir.join("test.h"), "#define A 1").unwrap();
fs::write(dir.join("test.o"), "").unwrap();
fs::write(&src, "int main() {}").unwrap();
fs::write(&header, "#define A 1").unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
fs::write(&obj, "").unwrap();
let d_content = format!(
"{}: {} {}",
obj.to_string_lossy(),
src.to_string_lossy(),
header.to_string_lossy()
);
fs::write(&d_file, d_content).unwrap();
let _guard = cwd_lock();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
fs::write("test.d", "test.o: test.c missing.h").unwrap();
assert!(
!needs_rebuild(&src.to_string_lossy(), &obj.to_string_lossy()),
"should be fresh"
needs_rebuild("test.c", "test.o"),
"missing dep in .d must force rebuild"
);
std::thread::sleep(std::time::Duration::from_millis(50));
fs::write(&header, "#define A 2").unwrap(); // modify header
fs::write("test.h", "#define A 1").unwrap();
std::thread::sleep(std::time::Duration::from_secs(2));
fs::write("test.o", "obj").unwrap();
fs::write("test.d", "test.o: test.c test.h").unwrap();
assert!(
needs_rebuild(&src.to_string_lossy(), &obj.to_string_lossy()),
!needs_rebuild("test.c", "test.o"),
"object newer than source and headers should be fresh"
);
std::thread::sleep(std::time::Duration::from_secs(2));
fs::write("test.h", "#define A 2").unwrap();
assert!(
needs_rebuild("test.c", "test.o"),
"changed header should trigger rebuild"
);
std::env::set_current_dir(prev).unwrap();
}
#[test]
fn parse_d_file_keeps_windows_path_separators() {
let content = r"target\obj\main.obj: src\main.c src\utils.h";
let deps = parse_d_file(content);
assert_eq!(deps, vec![r"src\main.c", r"src\utils.h"]);
}
#[test]

View File

@@ -950,7 +950,8 @@ fn absolutize_flag_path(flag: &str, project_root: &Path, prefixes: &[&str]) -> S
return flag.to_string();
}
let abs = project_root.join(rest);
return format!("{prefix}{}", abs.to_string_lossy());
let path = abs.to_string_lossy().replace('\\', "/");
return format!("{prefix}{path}");
}
}
flag.to_string()

View File

@@ -242,10 +242,13 @@ mod tests {
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(root.join("target/lib")).unwrap();
std::fs::create_dir_all(root.join("target").join("lib")).unwrap();
let mut paths = Vec::new();
push_default_lib_dirs(&mut paths, &root);
assert!(paths.iter().any(|p| p.ends_with("target/lib")));
assert!(paths.iter().any(|p| {
let n = p.replace('\\', "/");
n.ends_with("target/lib")
}));
let _ = std::fs::remove_dir_all(root);
}
}

View File

@@ -85,12 +85,7 @@ pub fn package_root_from_registry_info(pkg_info: &JsonValue) -> Result<PathBuf,
return Err("Registry package path is empty".to_string());
}
let path = Path::new(raw_path);
let full_path = if path.is_absolute() {
path.to_path_buf()
} else {
get_registry_cache_root().join(path)
};
let full_path = registry_path_to_pathbuf(raw_path.trim());
if full_path.file_name().and_then(|v| v.to_str()) == Some("dcr.toml") {
return full_path
@@ -102,6 +97,39 @@ pub fn package_root_from_registry_info(pkg_info: &JsonValue) -> Result<PathBuf,
Ok(full_path)
}
pub fn registry_path_to_pathbuf(raw: &str) -> PathBuf {
let s = raw.trim();
if let Some(rest) = s.strip_prefix("file://") {
if rest.len() >= 3 && rest.as_bytes()[0] == b'/' && rest.as_bytes()[2] == b':' {
return PathBuf::from(&rest[1..]);
}
if rest.len() >= 2 && rest.as_bytes()[1] == b':' {
return PathBuf::from(rest);
}
if rest.starts_with('/') {
return PathBuf::from(rest);
}
return PathBuf::from(format!("/{rest}"));
}
let mut s = s.replace('\\', "/");
if let Some(rest) = s.strip_prefix("//?/") {
s = rest.to_string();
} else if let Some(rest) = s.strip_prefix("//./") {
s = rest.to_string();
}
if s.len() >= 3 && s.as_bytes()[0] == b'/' && s.as_bytes()[2] == b':' {
s = s[1..].to_string();
}
let path = PathBuf::from(&s);
if path.is_absolute() || (s.len() >= 2 && s.as_bytes()[1] == b':') || s.starts_with('/') {
path
} else {
get_registry_cache_root().join(path)
}
}
pub fn registry_include_dir(dep_root: &Path) -> PathBuf {
dep_root.join("target").join("include")
}
@@ -219,16 +247,44 @@ mod tests {
#[test]
fn registry_package_root_accepts_package_dir_or_manifest_path() {
let pkg = serde_json::json!({ "path": "/tmp/pkg" });
let root = std::env::temp_dir().join(format!("dcr_reg_pkg_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).unwrap();
let root_str = root.to_string_lossy().replace('\\', "/");
let pkg = serde_json::json!({ "path": root_str });
assert_eq!(
package_root_from_registry_info(&pkg).unwrap(),
PathBuf::from("/tmp/pkg")
package_root_from_registry_info(&pkg)
.unwrap()
.to_string_lossy()
.replace('\\', "/"),
root_str
);
let pkg = serde_json::json!({ "path": "/tmp/pkg/dcr.toml" });
let manifest = format!("{root_str}/dcr.toml");
let pkg = serde_json::json!({ "path": manifest });
assert_eq!(
package_root_from_registry_info(&pkg).unwrap(),
PathBuf::from("/tmp/pkg")
package_root_from_registry_info(&pkg)
.unwrap()
.to_string_lossy()
.replace('\\', "/"),
root_str
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn registry_path_strips_windows_extended_prefix() {
let p = registry_path_to_pathbuf("//?/D:/cache/mylib");
let s = p.to_string_lossy().replace('\\', "/");
assert_eq!(s, "D:/cache/mylib");
let p2 = registry_path_to_pathbuf("/D:/cache/mylib");
let s2 = p2.to_string_lossy().replace('\\', "/");
assert_eq!(s2, "D:/cache/mylib");
let p3 = registry_path_to_pathbuf("file:///D:/cache/mylib");
let s3 = p3.to_string_lossy().replace('\\', "/");
assert_eq!(s3, "D:/cache/mylib");
}
}

View File

@@ -89,14 +89,17 @@ fn build_run_clean_flags_normal_project() {
let out = run_dcr_env(&["clean", "--release"], &dir, &envs);
assert!(out.status.success(), "dcr clean --release should succeed");
let target_dir = "target/x86_64-unknown-linux-gnu".to_string();
let release_dir = host_profile_dir(&dir, "release");
let debug_dir = host_profile_dir(&dir, "debug");
assert!(
!dir.join(&target_dir).join("release").exists(),
"target/x86_64-unknown-linux-gnu/release should be removed"
!release_dir.exists(),
"release profile dir should be removed: {}",
release_dir.display()
);
assert!(
dir.join(&target_dir).join("debug").is_dir(),
"target/x86_64-unknown-linux-gnu/debug should remain"
debug_dir.is_dir(),
"debug profile dir should remain: {}",
debug_dir.display()
);
let out = run_dcr_env(&["clean"], &dir, &envs);
@@ -123,16 +126,27 @@ fn build_with_target_config() {
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
if !out.status.success() {
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
if cfg!(windows) {
eprintln!("skipping linux-target path assert: build failed (no linux cross toolchain)");
return;
}
}
assert!(
out.status.success(),
"dcr build with target = \"linux\" should succeed"
);
let artifact = default_artifact_path(&dir, &project_name);
assert!(
artifact.is_file(),
"artifact should be at default path target/x86_64-unknown-linux-gnu/debug/{}",
project_name
let out_dir = dir
.join("target")
.join("x86_64-unknown-linux-gnu")
.join("debug");
assert_artifact_in(
&out_dir,
&project_name,
"build.target=linux should place artifact under target/x86_64-unknown-linux-gnu/debug",
);
}
@@ -164,11 +178,10 @@ fn build_with_out_dir() {
"dcr build with out_dir should succeed"
);
let artifact = dir.join("_BUILD").join(&project_name);
assert!(
artifact.is_file(),
"artifact should be at _BUILD/{} (custom out_dir)",
project_name
assert_artifact_in(
&dir.join("_BUILD"),
&project_name,
"artifact should be under custom out_dir _BUILD",
);
let default_path = default_artifact_path(&dir, &project_name);
@@ -225,15 +238,12 @@ roots = ["src"]
assert!(out.status.success(), "flat-bin nasm build should succeed");
let candidates = [
host_profile_dir(&dir, "debug").join("boot.bin"),
dir.join("target/debug/boot.bin"),
dir.join("target").join(format!(
"{}-unknown-linux-gnu/debug/boot.bin",
std::env::consts::ARCH
)),
];
assert!(
candidates.iter().any(|p| p.is_file()),
"expected boot.bin under target/…/debug, candidates: {candidates:?}"
"expected boot.bin under host profile dir, candidates: {candidates:?}"
);
}
@@ -256,13 +266,23 @@ fn package_build_target_used_without_cli_flag() {
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
if !out.status.success() {
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
if cfg!(windows) {
eprintln!("skipping linux-target path assert: build failed (no linux cross toolchain)");
return;
}
}
assert!(out.status.success(), "build should use package target");
let artifact = dir
.join("target/x86_64-unknown-linux-gnu/debug")
.join(&project_name);
assert!(
artifact.is_file(),
"artifact should be under package build.target path"
let out_dir = dir
.join("target")
.join("x86_64-unknown-linux-gnu")
.join("debug");
assert_artifact_in(
&out_dir,
&project_name,
"artifact should be under package build.target path",
);
}

View File

@@ -117,12 +117,24 @@ fn registry_dependency_is_built_from_cache() {
"[registry.local]\nurl = \"file://local\"\npriority = 1\n",
)
.expect("failed to write registry config");
let dep_abs = dep.canonicalize().unwrap_or(dep.clone());
let mut dep_path = dep_abs.to_string_lossy().replace('\\', "/");
if let Some(rest) = dep_path.strip_prefix("//?/") {
dep_path = rest.to_string();
}
if dep_path.len() >= 3 && dep_path.as_bytes()[0] == b'/' && dep_path.as_bytes()[2] == b':' {
dep_path = dep_path[1..].to_string();
}
std::fs::write(
dcr_home.join("index.json"),
format!(
"{{\"packages\":[{{\"name\":\"mylib\",\"latest_version\":\"0.1.0\",\"path\":\"{}\"}}]}}",
dep.display()
),
serde_json::json!({
"packages": [{
"name": "mylib",
"latest_version": "0.1.0",
"path": dep_path
}]
})
.to_string(),
)
.expect("failed to write registry index");
@@ -151,11 +163,16 @@ fn registry_dependency_is_built_from_cache() {
.expect("failed to write app source");
let index_path = dcr_home.join("index.json");
let envs = [
let home_s = home.to_string_lossy().to_string();
let index_s = index_path.to_string_lossy().to_string();
let mut envs = vec![
("DCR_COMPILER", compiler),
("HOME", home.to_str().unwrap()),
("DCR_INDEX_PATH", index_path.to_str().unwrap()),
("HOME", home_s.as_str()),
("DCR_INDEX_PATH", index_s.as_str()),
];
if cfg!(windows) {
envs.push(("USERPROFILE", home_s.as_str()));
}
let out = run_dcr_env(&["build"], &app, &envs);
if !out.status.success() {
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));

View File

@@ -74,6 +74,32 @@ fn available_compiler() -> Option<&'static str> {
None
}
fn host_profile_dir(project_root: &Path, profile: &str) -> PathBuf {
let target = project_root.join("target");
if cfg!(target_os = "linux") {
let arch = std::env::consts::ARCH;
let env = if cfg!(target_env = "musl") {
"musl"
} else {
"gnu"
};
target
.join(format!("{arch}-unknown-linux-{env}"))
.join(profile)
} else if cfg!(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)) {
let arch = std::env::consts::ARCH;
let os = std::env::consts::OS;
target.join(format!("{arch}-unknown-{os}")).join(profile)
} else {
target.join(profile)
}
}
// --- Tests ---
#[test]
@@ -223,25 +249,24 @@ fn clean_specific_profile() {
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "init should succeed");
// Create target/x86_64-unknown-linux-gnu/debug and release dirs
let target_base = "target/x86_64-unknown-linux-gnu";
std::fs::create_dir_all(dir.join(target_base).join("debug")).expect("create debug");
std::fs::write(dir.join(target_base).join("debug").join("dummy.o"), "x").expect("write");
std::fs::create_dir_all(dir.join(target_base).join("release")).expect("create release");
std::fs::write(dir.join(target_base).join("release").join("dummy.o"), "x").expect("write");
let debug_dir = host_profile_dir(&dir, "debug");
let release_dir = host_profile_dir(&dir, "release");
std::fs::create_dir_all(&debug_dir).expect("create debug");
std::fs::write(debug_dir.join("dummy.o"), "x").expect("write");
std::fs::create_dir_all(&release_dir).expect("create release");
std::fs::write(release_dir.join("dummy.o"), "x").expect("write");
// Clean only release
let out = run_dcr(&["clean", "--release"], &dir);
assert!(out.status.success(), "clean --release should succeed");
assert!(
!dir.join(target_base).join("release").exists(),
"target/x86_64-unknown-linux-gnu/release should be removed"
!release_dir.exists(),
"release profile dir should be removed: {}",
release_dir.display()
);
assert!(
dir.join("target/x86_64-unknown-linux-gnu")
.join("debug")
.is_dir(),
"target/x86_64-unknown-linux-gnu/debug should remain"
debug_dir.is_dir(),
"debug profile dir should remain: {}",
debug_dir.display()
);
}
@@ -279,19 +304,20 @@ fn staticlib_build() {
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
assert!(out.status.success(), "staticlib build should succeed");
// Check that a .a file exists
let lib_path = dir
.join("target")
.join("x86_64-unknown-linux-gnu")
.join("debug");
let lib_path = host_profile_dir(&dir, "debug");
let has_lib = std::fs::read_dir(&lib_path)
.map(|entries| {
entries
.filter_map(|e| e.ok())
.any(|e| e.file_name().to_string_lossy().ends_with(".a"))
entries.filter_map(|e| e.ok()).any(|e| {
let name = e.file_name().to_string_lossy().to_string();
name.ends_with(".a") || name.ends_with(".lib")
})
})
.unwrap_or(false);
assert!(has_lib, "staticlib should produce a .a file");
assert!(
has_lib,
"staticlib should produce a .a/.lib in {}",
lib_path.display()
);
}
#[test]
@@ -307,12 +333,11 @@ fn build_release_profile() {
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build", "--release"], &dir, &envs);
assert!(out.status.success(), "release build should succeed");
let release_dir = host_profile_dir(&dir, "release");
assert!(
dir.join("target")
.join("x86_64-unknown-linux-gnu")
.join("release")
.is_dir(),
"target/x86_64-unknown-linux-gnu/release should exist"
release_dir.is_dir(),
"release profile dir should exist: {}",
release_dir.display()
);
}

View File

@@ -56,14 +56,17 @@ fn workspace_build_and_clean_all() {
"clean --all should not spam missing member target/: {combined}"
);
let target_dir = "target/x86_64-unknown-linux-gnu";
let release_dir = host_profile_dir(&root, "release");
let debug_dir = host_profile_dir(&root, "debug");
assert!(
!root.join(target_dir).join("release").exists(),
"root target/x86_64-unknown-linux-gnu/release should be removed"
!release_dir.exists(),
"root release profile dir should be removed: {}",
release_dir.display()
);
assert!(
root.join(target_dir).join("debug").exists(),
"root target/x86_64-unknown-linux-gnu/debug should remain"
debug_dir.exists(),
"root debug profile dir should remain: {}",
debug_dir.display()
);
let out = run_dcr_env(&["clean", "--all"], &root, &envs);

View File

@@ -92,10 +92,63 @@ pub fn parse_project_name(toml: &str) -> String {
}
#[allow(dead_code)]
pub fn default_artifact_path(project_root: &Path, project_name: &str) -> PathBuf {
project_root
.join("target")
.join("x86_64-unknown-linux-gnu")
.join("debug")
.join(project_name)
pub fn host_profile_dir(project_root: &Path, profile: &str) -> PathBuf {
let target = project_root.join("target");
if cfg!(target_os = "linux") {
let arch = std::env::consts::ARCH;
let env = if cfg!(target_env = "musl") {
"musl"
} else {
"gnu"
};
target
.join(format!("{arch}-unknown-linux-{env}"))
.join(profile)
} else if cfg!(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)) {
let arch = std::env::consts::ARCH;
let os = std::env::consts::OS;
target.join(format!("{arch}-unknown-{os}")).join(profile)
} else {
target.join(profile)
}
}
#[allow(dead_code)]
pub fn bin_name(project_name: &str) -> String {
if cfg!(windows) {
format!("{project_name}.exe")
} else {
project_name.to_string()
}
}
#[allow(dead_code)]
pub fn default_artifact_path(project_root: &Path, project_name: &str) -> PathBuf {
host_profile_dir(project_root, "debug").join(bin_name(project_name))
}
#[allow(dead_code)]
pub fn artifact_candidates(dir: &Path, project_name: &str) -> Vec<PathBuf> {
let base = bin_name(project_name);
let bare = project_name.to_string();
vec![
dir.join(&base),
dir.join(&bare),
dir.join(format!("{project_name}.exe")),
]
}
#[allow(dead_code)]
pub fn assert_artifact_in(dir: &Path, project_name: &str, ctx: &str) {
let candidates = artifact_candidates(dir, project_name);
assert!(
candidates.iter().any(|p| p.is_file()),
"{ctx}: expected binary in {}, candidates: {candidates:?}",
dir.display()
);
}