#!/bin/sh # Sidekit installer — https://sidekit.net # # Usage: curl -fsSL https://sidekit.net/install.sh | sh # # Sidekit is a .NET global tool, and this script is a wrapper around # `dotnet tool install`, not a replacement for it. It cannot ship a # prerequisite-free binary: RoslynWorkspaceLoader calls MSBuildLocator, which loads # the *installed SDK's* MSBuild at runtime, and the test runner shells out to # `dotnet`. So the SDK is a hard requirement no packaging choice can remove — which # is why this script's first job is to check for it and say so plainly, rather than # let the failure surface later as a confusing Roslyn error. # # The package is served from GitHub Releases on the public repository. This script is # the indirection layer: it is fetched fresh from sidekit.net every time, so the # download host can move without the install command people share ever changing. # # Environment overrides: # SIDEKIT_VERSION version to install (default: whatever latest.txt names) # SIDEKIT_INSTALL_DIR install here via --tool-path instead of --global # SIDEKIT_BASE_URL releases host (default: the public repo's releases) set -eu SIDEKIT_BASE_URL="${SIDEKIT_BASE_URL:-https://github.com/sidekit-net/sidekit/releases}" MIN_SDK_MAJOR=10 # Everything lives in main() so that a truncated download — the failure mode unique # to `curl | sh` — cannot execute a partial script. sh reads the file as it arrives; # a half-received function body is a syntax error, and main() never runs. main() { say "Sidekit installer" need_downloader check_dotnet_sdk version="${SIDEKIT_VERSION:-$(resolve_latest_version)}" [ -n "$version" ] || die "could not determine which version to install. Could not read ${SIDEKIT_BASE_URL}/latest/download/latest.txt. Check that the host is reachable, or pin a version: SIDEKIT_VERSION=x.y.z" tmp="$(mktemp -d 2>/dev/null || mktemp -d -t sidekit)" # shellcheck disable=SC2064 # $tmp is expanded now, deliberately. trap "rm -rf '$tmp'" EXIT INT TERM # Saved under NuGet's canonical ..nupkg name whatever the URL was: # --add-source reads the directory as a local feed and the filename is part of # that contract. nupkg="Sidekit.${version}.nupkg" url="${SIDEKIT_BASE_URL}/download/v${version}/${nupkg}" say "Downloading ${nupkg}" download "$url" "${tmp}/${nupkg}" || die "download failed: ${url}" install_tool "$version" "$tmp" check_path say "" say "Installed. Next: run 'sidekit doctor' in a .NET repository." } say() { printf '%s\n' "$*"; } warn() { printf '\n! %s\n' "$*" >&2; } die() { printf '\nerror: %s\n' "$*" >&2; exit 1; } need_downloader() { if command -v curl >/dev/null 2>&1; then DOWNLOADER=curl elif command -v wget >/dev/null 2>&1; then DOWNLOADER=wget else die "neither curl nor wget found; one of them is required." fi } download() { case "$DOWNLOADER" in curl) curl -fsSL "$1" -o "$2" ;; wget) wget -qO "$2" "$1" ;; esac } download_stdout() { case "$DOWNLOADER" in curl) curl -fsSL "$1" ;; wget) wget -qO- "$1" ;; esac } # The SDK, not just the runtime: MSBuildLocator needs a real SDK installation to # register against. `dotnet --version` is the wrong probe — it resolves through any # global.json in the current directory, so it reports what this directory wants # rather than what is installed. --list-sdks reports the truth. check_dotnet_sdk() { command -v dotnet >/dev/null 2>&1 \ || die "the .NET SDK is required but 'dotnet' is not on PATH. Sidekit builds and runs your tests on the real toolchain, so the SDK is not optional. Install .NET ${MIN_SDK_MAJOR} from https://dotnet.microsoft.com/download" sdks="$(dotnet --list-sdks 2>/dev/null || true)" [ -n "$sdks" ] || die "'dotnet' is on PATH but reports no installed SDKs. This usually means only the runtime is installed. Sidekit needs the SDK: https://dotnet.microsoft.com/download" newest="$(printf '%s\n' "$sdks" | awk -F. '{print $1}' | sort -rn | head -n1)" case "$newest" in ''|*[!0-9]*) die "could not parse 'dotnet --list-sdks' output." ;; esac if [ "$newest" -lt "$MIN_SDK_MAJOR" ]; then die ".NET ${MIN_SDK_MAJOR} SDK or newer is required; the newest installed is ${newest}. Install it from https://dotnet.microsoft.com/download" fi # dotnet --list-sdks emits ascending order, so the last line matching the highest # major is the highest patch of it. Reported in full because "which SDK?" is the # first question any install problem raises. newest_full="$(printf '%s\n' "$sdks" | awk -F'[. ]' -v m="$newest" '$1==m {v=$1"."$2"."$3} END {print v}')" say "Found .NET SDK ${newest_full}." } # Read from a release asset rather than the GitHub API: the API allows 60 # unauthenticated requests per hour per IP, which one corporate NAT exhausts on # everyone's behalf, and parsing its JSON in POSIX sh would be worse than this. # # /releases/latest resolves to the newest release NOT marked as a prerelease — which # is why release.yml does not mark betas as prereleases, however tempting. resolve_latest_version() { download_stdout "${SIDEKIT_BASE_URL}/latest/download/latest.txt" 2>/dev/null \ | tr -d ' \t\r\n' } install_tool() { version="$1" source_dir="$2" if [ -n "${SIDEKIT_INSTALL_DIR:-}" ]; then mkdir -p "$SIDEKIT_INSTALL_DIR" set -- --tool-path "$SIDEKIT_INSTALL_DIR" else set -- --global fi # `dotnet tool install` fails outright if the tool is already present, so an # installer that only ever installs breaks on its second run — which is exactly # how people upgrade. Pick the verb from what is actually installed. if dotnet tool list "$@" 2>/dev/null | grep -qi '^sidekit[[:space:]]'; then say "Updating Sidekit to ${version}" verb=update else say "Installing Sidekit ${version}" verb=install fi # --ignore-failed-sources: the package comes from the local directory, but NuGet # still consults every configured source. A restricted corporate proxy failing on # an unrelated feed must not block an install whose package is already on disk. dotnet tool "$verb" "$@" Sidekit \ --version "$version" \ --add-source "$source_dir" \ --ignore-failed-sources \ || die "'dotnet tool ${verb}' failed." } check_path() { if [ -n "${SIDEKIT_INSTALL_DIR:-}" ]; then target="$SIDEKIT_INSTALL_DIR" else target="${DOTNET_TOOLS_PATH:-$HOME/.dotnet/tools}" fi case ":${PATH}:" in *":${target}:"*) return 0 ;; esac # Not fatal: the tool is installed and correct, it just is not reachable yet. warn "${target} is not on your PATH." say " Add it to your shell profile, then reopen the shell:" say " export PATH=\"\$PATH:${target}\"" } main "$@"