#!/usr/bin/env bash set -Eeuo pipefail DCRUP_HOME="${DCRUP_HOME:-$HOME/.dcr}" DCRUP_BIN="${DCRUP_BIN:-$DCRUP_HOME/bin}" DCRUP_TC="${DCRUP_HOME}/toolchains" DCRUP_META="${DCRUP_HOME}/meta" REPO_URL="${DCRUP_REPO:-https://github.com/dexoron/dcr}" DCRUP_INSTALL_URL="${DCRUP_INSTALL_URL:-https://sh.dcr-tool.ru}" DCRUP_MIRROR="${DCRUP_MIRROR:-auto}" API_LATEST="https://api.github.com/repos/dexoron/dcr/releases/latest" API_ALL="https://api.github.com/repos/dexoron/dcr/releases" FEATURES="${DCR_FEATURES:-archive}" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' log() { printf "${CYAN}[dcrup]${NC} %s\n" "$*"; } ok() { printf "${GREEN}[ok]${NC} %s\n" "$*"; } warn() { printf "${YELLOW}[warn]${NC} %s\n" "$*"; } die() { printf "${RED}[error]${NC} %s\n" "$*" >&2 exit 1 } usage() { cat <<'EOF' dcrup — install and switch DCR versions Usage: dcrup install [--build|--release] [--force] [--libc gnu|musl|auto] [--mirror auto|cf|gh|sf|ya] dcrup default dcrup update dcrup list dcrup show dcrup which dcrup uninstall [|--all] dcrup self-install [--to DIR] dcrup help Spec: stable | dev | night VERSION → VERSION@stable (e.g. 0.8.2) VERSION@stable VERSION@dev night → always build from branch "dev" HEAD Modes: --release download prebuilt binary (default for stable/dev) --build cargo build --release --features archive night always --build (prebuilt not available) Linux libc (prebuilt triple only): --libc gnu x86_64-unknown-linux-gnu (default) --libc musl x86_64-unknown-linux-musl --libc auto musl if host ldd/libc is musl, else gnu Mirrors (prebuilt download only): --mirror auto try cf → gh → sf → ya, first successful download wins (default) --mirror cf download from cdn-global.dcr-tool.ru --mirror gh download from github.com/dexoron/dcr --mirror sf download from sourceforge.net/projects/dcr-tool --mirror ya download from cdn-ru.dcr-tool.ru DCRUP_MIRROR env override (default: auto) Env: DCRUP_HOME default ~/.dcr DCRUP_BIN default $DCRUP_HOME/bin DCR_FEATURES default archive DCRUP_REPO git/GitHub repo URL DCRUP_LIBC gnu|musl|auto (default: gnu) DCRUP_MIRROR auto|cf|gh|sf|ya (default: auto) EOF } need_cmd() { command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" } detect_linux_libc() { local pref="${DCRUP_LIBC:-gnu}" case "$pref" in gnu | glibc) echo "gnu" ;; musl) echo "musl" ;; auto) if command -v ldd >/dev/null 2>&1; then if ldd --version 2>&1 | grep -qi musl; then echo "musl" return 0 fi fi if [[ -e /lib/ld-musl-x86_64.so.1 || -e /lib/ld-musl-aarch64.so.1 ]]; then echo "musl" return 0 fi if command -v getconf >/dev/null 2>&1 && getconf GNU_LIBC_VERSION >/dev/null 2>&1; then echo "gnu" return 0 fi echo "gnu" ;; *) die "invalid DCRUP_LIBC / --libc: $pref (use gnu|musl|auto)" ;; esac } detect_triple() { local os arch libc os="$(uname -s)" arch="$(uname -m)" case "$os:$arch" in Linux:x86_64) libc="$(detect_linux_libc)" if [[ "$libc" == "musl" ]]; then echo "x86_64-unknown-linux-musl" else echo "x86_64-unknown-linux-gnu" fi ;; Linux:aarch64 | Linux:arm64) libc="$(detect_linux_libc)" if [[ "$libc" == "musl" ]]; then echo "aarch64-unknown-linux-musl" else echo "aarch64-unknown-linux-gnu" fi ;; Linux:i686 | Linux:i386) libc="$(detect_linux_libc)" if [[ "$libc" == "musl" ]]; then echo "i686-unknown-linux-musl" else echo "i686-unknown-linux-gnu" fi ;; Linux:armv7l | Linux:armv7) libc="$(detect_linux_libc)" if [[ "$libc" == "musl" ]]; then echo "armv7-unknown-linux-musleabihf" else echo "armv7-unknown-linux-gnueabihf" fi ;; Linux:riscv64) libc="$(detect_linux_libc)" if [[ "$libc" == "musl" ]]; then echo "riscv64gc-unknown-linux-musl" else echo "riscv64gc-unknown-linux-gnu" fi ;; Darwin:x86_64) echo "x86_64-apple-darwin" ;; Darwin:arm64 | Darwin:aarch64) echo "aarch64-apple-darwin" ;; FreeBSD:x86_64 | FreeBSD:amd64) echo "x86_64-unknown-freebsd" ;; FreeBSD:aarch64 | FreeBSD:arm64) echo "aarch64-unknown-freebsd" ;; OpenBSD:x86_64 | OpenBSD:amd64) echo "x86_64-unknown-openbsd" ;; NetBSD:x86_64 | NetBSD:amd64) echo "x86_64-unknown-netbsd" ;; MINGW* | MSYS* | CYGWIN*) case "$arch" in x86_64 | amd64) echo "x86_64-pc-windows-gnu" ;; aarch64 | arm64) echo "aarch64-pc-windows-gnullvm" ;; i686 | i386) echo "i686-pc-windows-gnu" ;; *) die "unsupported Windows arch under MSYS: $arch" ;; esac ;; *) die "unsupported platform: $os/$arch" ;; esac } normalize_version() { local v="$1" v="${v#v}" printf '%s' "$v" } parse_spec() { local raw="$1" SPEC_RAW="$raw" SPEC_CHANNEL="" SPEC_VERSION="" SPEC_FLOATING=0 case "$raw" in "") die "missing version/channel spec" ;; stable | dev | night) SPEC_CHANNEL="$raw" SPEC_VERSION="" SPEC_FLOATING=1 ;; *@*) SPEC_VERSION="$(normalize_version "${raw%@*}")" SPEC_CHANNEL="${raw#*@}" case "$SPEC_CHANNEL" in stable | dev) ;; night) die "cannot pin a version on night (use: dcrup install night)" ;; *) die "unknown channel in spec: $SPEC_CHANNEL (use stable|dev)" ;; esac [[ -n "$SPEC_VERSION" ]] || die "empty version in spec: $raw" ;; *) SPEC_VERSION="$(normalize_version "$raw")" SPEC_CHANNEL="stable" [[ -n "$SPEC_VERSION" ]] || die "invalid spec: $raw" ;; esac } toolchain_id() { if [[ "$SPEC_CHANNEL" == "night" ]]; then local sha="${1:-unknown}" echo "night-${sha}" elif [[ "$SPEC_FLOATING" -eq 1 ]]; then local ver="${1:-latest}" echo "${SPEC_CHANNEL}-${ver}" else echo "${SPEC_VERSION}-${SPEC_CHANNEL}" fi } ensure_dirs() { mkdir -p "$DCRUP_TC" "$DCRUP_BIN" "$DCRUP_META" } active_file() { echo "$DCRUP_META/active"; } read_active() { if [[ -f "$(active_file)" ]]; then cat "$(active_file)" fi } write_active() { printf '%s\n' "$1" >"$(active_file)" } link_default() { local id="$1" local src="$DCRUP_TC/$id/dcr" [[ -x "$src" ]] || die "toolchain not installed: $id" # Resolve the source so a relative DCRUP_HOME cannot create a broken link. src="$(cd "$(dirname "$src")" && pwd)/$(basename "$src")" ln -sfn "$src" "$DCRUP_BIN/dcr" write_active "$id" ok "default → $id ($DCRUP_BIN/dcr)" } fetch_json() { need_cmd curl curl -fsSL "$1" } json_tag() { printf '%s' "$1" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1 } set_mirror() { case "$1" in auto | cf | gh | sf | ya) DCRUP_MIRROR="$1" ;; *) die "invalid --mirror: $1 (use auto|cf|gh|sf|ya)" ;; esac } mirror_url() { local mirror="$1" tag="$2" asset="$3" case "$mirror" in cf) printf 'https://cdn-global.dcr-tool.ru/%s/%s' "$tag" "$asset" ;; gh) printf 'https://github.com/dexoron/dcr/releases/download/%s/%s' "$tag" "$asset" ;; sf) printf 'https://sourceforge.net/projects/dcr-tool/files/%s/%s/download' "$tag" "$asset" ;; ya) printf 'https://cdn-ru.dcr-tool.ru/%s/%s' "$tag" "$asset" ;; *) return 1 ;; esac } mirror_chain() { echo "cf gh sf ya"; } mirrors_to_try() { case "$DCRUP_MIRROR" in auto) mirror_chain ;; cf | gh | sf | ya) echo "$DCRUP_MIRROR" ;; *) die "invalid mirror: $DCRUP_MIRROR (use auto|cf|gh|sf|ya)" ;; esac } download_from_mirrors() { local tag="$1" asset="$2" dest="$3" local tmp="$dest/dcr.download" m url size USED_MIRROR="" for m in $(mirrors_to_try); do url="$(mirror_url "$m" "$tag" "$asset")" log "trying mirror $m …" rm -f "$tmp" if curl -fL --retry 2 --retry-delay 1 --connect-timeout 10 --max-time 180 \ -A "dcrup" "$url" -o "$tmp"; then size="$(wc -c <"$tmp" | tr -d ' ')" if [[ "${size:-0}" -gt 100000 ]]; then mv "$tmp" "$dest/dcr" USED_MIRROR="$m" return 0 fi warn "mirror $m returned a too-small file (${size:-0} bytes)" else warn "mirror $m failed" fi rm -f "$tmp" done return 1 } latest_stable_json() { fetch_json "$API_LATEST" } latest_dev_json() { need_cmd curl local json result json="$(fetch_json "$API_ALL")" if command -v jq >/dev/null 2>&1; then result="$(printf '%s' "$json" | jq -e -c '[.[] | select(.prerelease == true)][0]')" || true elif command -v python3 >/dev/null 2>&1; then result="$(printf '%s' "$json" | python3 -c ' import sys, json rels = json.load(sys.stdin) pre = [r for r in rels if r.get("prerelease")] print(json.dumps(pre[0]) if pre else "", end="") ')" else die "dev channel needs jq or python3 to parse GitHub API" fi [[ -n "$result" && "$result" != "null" ]] || die "no prerelease (dev) found" printf '%s' "$result" } release_json_for_spec() { if [[ "$SPEC_FLOATING" -eq 1 ]]; then case "$SPEC_CHANNEL" in stable) latest_stable_json ;; dev) latest_dev_json ;; night) die "night has no GitHub release binary" ;; esac else fetch_json "https://api.github.com/repos/dexoron/dcr/releases/tags/v${SPEC_VERSION}" fi } install_prebuilt() { local triple asset tag version id dest mirror need_cmd curl triple="$(detect_triple)" json="$(release_json_for_spec)" tag="$(json_tag "$json")" [[ -n "$tag" ]] || die "failed to parse release tag" version="$(normalize_version "$tag")" asset="dcr-${triple}-${version}" case "$triple" in *windows*) asset="${asset}.exe" ;; esac id="$(toolchain_id "$version")" dest="$DCRUP_TC/$id" if [[ -x "$dest/dcr" && "${FORCE:-0}" != "1" ]]; then ok "already installed: $id" link_default "$id" return 0 fi mkdir -p "$dest" log "downloading $asset (mirror=$DCRUP_MIRROR) …" download_from_mirrors "$tag" "$asset" "$dest" || die "no mirror reachable for $asset (try: --mirror cf|gh|sf|ya)" mirror="$USED_MIRROR" log "mirror → $mirror" chmod +x "$dest/dcr" printf '%s\n' \ "channel=$SPEC_CHANNEL" \ "version=$version" \ "tag=$tag" \ "source=release" \ "triple=$triple" \ "mirror=$mirror" \ "libc=${DCRUP_LIBC:-gnu}" \ >"$dest/dcrup-meta" ok "installed prebuilt $id ($triple via $mirror)" link_default "$id" } install_build() { local tmp id dest sha version branch_or_tag need_cmd git need_cmd cargo tmp="$(mktemp -d "${TMPDIR:-/tmp}/dcrup.XXXXXX")" cleanup_tmp() { rm -rf "$tmp"; } trap cleanup_tmp EXIT if [[ "$SPEC_CHANNEL" == "night" ]]; then log "cloning $REPO_URL (branch dev) for night …" git clone --depth 1 --branch dev "$REPO_URL" "$tmp/src" 2>/dev/null || git clone --depth 1 "$REPO_URL" "$tmp/src" sha="$(git -C "$tmp/src" rev-parse --short HEAD)" version="night" id="$(toolchain_id "$sha")" elif [[ "$SPEC_FLOATING" -eq 1 ]]; then local json tag json="$(release_json_for_spec)" tag="$(json_tag "$json")" [[ -n "$tag" ]] || die "failed to resolve floating channel tag" version="$(normalize_version "$tag")" id="$(toolchain_id "$version")" log "cloning $REPO_URL @ $tag …" git clone --depth 1 --branch "$tag" "$REPO_URL" "$tmp/src" 2>/dev/null || { git clone --depth 1 "$REPO_URL" "$tmp/src" git -C "$tmp/src" fetch --depth 1 origin "refs/tags/${tag}:refs/tags/${tag}" git -C "$tmp/src" checkout "$tag" } sha="$(git -C "$tmp/src" rev-parse --short HEAD)" else version="$SPEC_VERSION" id="$(toolchain_id)" local tag="v${SPEC_VERSION}" log "cloning $REPO_URL @ $tag …" git clone --depth 1 --branch "$tag" "$REPO_URL" "$tmp/src" 2>/dev/null || { git clone --depth 1 "$REPO_URL" "$tmp/src" git -C "$tmp/src" fetch --depth 1 origin "refs/tags/${tag}:refs/tags/${tag}" git -C "$tmp/src" checkout "$tag" } sha="$(git -C "$tmp/src" rev-parse --short HEAD)" fi dest="$DCRUP_TC/$id" if [[ -x "$dest/dcr" && "${FORCE:-0}" != "1" ]]; then ok "already installed: $id" link_default "$id" return 0 fi log "building release (features: $FEATURES) …" ( cd "$tmp/src" # shellcheck disable=SC2086 cargo build --release --features $FEATURES ) mkdir -p "$dest" cp "$tmp/src/target/release/dcr" "$dest/dcr" chmod +x "$dest/dcr" printf '%s\n' "channel=$SPEC_CHANNEL" "version=$version" "git=$sha" "source=build" "features=$FEATURES" >"$dest/dcrup-meta" ok "installed build $id" link_default "$id" trap - EXIT cleanup_tmp } cmd_install() { local mode="release" FORCE=0 local spec="" local libc_cli="" while [[ $# -gt 0 ]]; do case "$1" in --build) mode="build" shift ;; --release) mode="release" shift ;; --force) FORCE=1 shift ;; --libc) [[ $# -ge 2 ]] || die "--libc needs gnu|musl|auto" libc_cli="$2" shift 2 ;; --libc=*) libc_cli="${1#--libc=}" shift ;; --mirror) [[ $# -ge 2 ]] || die "--mirror needs auto|cf|gh|sf|ya" set_mirror "$2" shift 2 ;; --mirror=*) set_mirror "${1#--mirror=}" shift ;; -h | --help) usage exit 0 ;; -*) die "unknown flag: $1" ;; *) [[ -z "$spec" ]] || die "unexpected argument: $1" spec="$1" shift ;; esac done [[ -n "$spec" ]] || die "usage: dcrup install [--build|--release] [--force] [--libc gnu|musl|auto] [--mirror auto|cf|gh|sf|ya]" parse_spec "$spec" ensure_dirs if [[ -n "$libc_cli" ]]; then case "$libc_cli" in gnu | glibc | musl | auto) DCRUP_LIBC="$libc_cli" ;; *) die "invalid --libc: $libc_cli (use gnu|musl|auto)" ;; esac fi export DCRUP_LIBC="${DCRUP_LIBC:-gnu}" if [[ "$SPEC_CHANNEL" == "night" ]]; then mode="build" fi export FORCE log "install spec=$SPEC_RAW channel=$SPEC_CHANNEL version=${SPEC_VERSION:-∅} mode=$mode libc=$DCRUP_LIBC mirror=$DCRUP_MIRROR" if [[ "$mode" == "build" ]]; then install_build else install_prebuilt fi check_path } cmd_default() { [[ $# -eq 1 ]] || die "usage: dcrup default " 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 |--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 || true)" if [[ -z "$self" || ! -f "$self" ]]; then # When invoked as `curl ... | sh`, the shell has no script file to copy. # Fetch the same trusted installer URL and install that exact script. need_cmd curl self="$(mktemp "${TMPDIR:-/tmp}/dcrup.XXXXXX")" curl -fsSL --proto '=https' --tlsv1.2 "$DCRUP_INSTALL_URL" -o "$self" || { rm -f "$self" die "unable to download installer for self-install" } trap 'rm -f "$self"' EXIT fi 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 "$@"