diff --git a/DEV.md b/DEV.md index 0cf7b95..e1a41d9 100644 --- a/DEV.md +++ b/DEV.md @@ -136,6 +136,13 @@ The project includes integration tests that run the binary against sample projec - `tests/check.rs`: Tests the `check` command - `tests/spec.rs`: Tests the `spec` command +#### scopelintup +To test the version manager (install, shim, version resolution, error handling) in an isolated temp dir: + +```bash +./scopelintup/test-scopelintup.sh +``` + #### Test Projects Sample projects in `tests/` are used for integration testing: - `check-proj1-AllFindings/`: Project with known validation issues diff --git a/README.md b/README.md index 716bc5d..ec33b34 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,30 @@ A simple and opinionated tool designed for basic formatting/linting of Solidity ## Installation +### Recommended: `scopelintup` + +Install `scopelintup`, the scopelint version manager, to manage versions automatically per-project: + +```bash +curl -L https://raw.githubusercontent.com/ScopeLift/scopelint/main/scopelintup/install | bash +``` + +Then install the latest version of scopelint: + +```bash +scopelintup +``` + +Or install a specific version: + +```bash +scopelintup --version v0.1.0 +``` + +Projects can pin a version by adding a `.scopelint-version` file (e.g. containing `v0.1.0`). When a pinned version is present, the `scopelint` command automatically uses the correct binary. + +### Alternative: `cargo install` + When using the [ScopeLift Foundry template](https://github.com/ScopeLift/foundry-template/) scopelint will automatically be ran in CI. To run locally: 1. Install the [rust toolchain](https://www.rust-lang.org/tools/install) diff --git a/scopelintup/install b/scopelintup/install new file mode 100755 index 0000000..8d3d5f8 --- /dev/null +++ b/scopelintup/install @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +set -euo pipefail + +# scopelintup bootstrap installer +# Usage: curl -L https://raw.githubusercontent.com/ScopeLift/scopelint/main/scopelintup/install | bash + +SCOPELINT_DIR="${SCOPELINT_DIR:-$HOME/.scopelint}" +SCOPELINT_BIN_DIR="$SCOPELINT_DIR/bin" +REPO="ScopeLift/scopelint" +SCOPELINTUP_URL="https://raw.githubusercontent.com/$REPO/main/scopelintup/scopelintup" + +say() { printf "\033[1;32mscopelintup\033[0m: %s\n" "$*"; } +err() { printf "\033[1;31mscopelintup\033[0m: error: %s\n" "$*" >&2; exit 1; } + +need_cmd() { + command -v "$1" > /dev/null 2>&1 || err "need '$1' (command not found)" +} + +need_cmd curl + +# 1. Create directory structure. +say "creating $SCOPELINT_DIR ..." +mkdir -p "$SCOPELINT_BIN_DIR" +mkdir -p "$SCOPELINT_DIR/versions" + +# 2. Download scopelintup. +say "downloading scopelintup..." +curl -fsSL -o "$SCOPELINT_DIR/scopelintup" "$SCOPELINTUP_URL" +chmod +x "$SCOPELINT_DIR/scopelintup" + +# 3. Symlink scopelintup into bin dir. +ln -sf "$SCOPELINT_DIR/scopelintup" "$SCOPELINT_BIN_DIR/scopelintup" + +# 4. Write the shim by invoking scopelintup's write_shim (just generate inline). +cat > "$SCOPELINT_BIN_DIR/scopelint" <<'SHIM' +#!/usr/bin/env bash +set -euo pipefail + +SCOPELINT_DIR="${SCOPELINT_DIR:-$HOME/.scopelint}" + +resolve_from_file() { + local dir="$PWD" + while [[ "$dir" != "/" ]]; do + if [[ -f "$dir/.scopelint-version" ]]; then + cat "$dir/.scopelint-version" + return + fi + dir="$(dirname "$dir")" + done +} + +version=$(resolve_from_file) + +if [[ -z "${version:-}" ]]; then + version="${SCOPELINT_VERSION:-}" +fi + +if [[ -z "${version:-}" && -f "$SCOPELINT_DIR/.current_version" ]]; then + version=$(cat "$SCOPELINT_DIR/.current_version") +fi + +if [[ -z "${version:-}" ]]; then + echo "scopelint: no version configured." >&2 + echo " Run 'scopelintup' to install the latest version," >&2 + echo " or create a .scopelint-version file." >&2 + exit 1 +fi + +[[ "$version" == v* ]] || version="v$version" + +binary="$SCOPELINT_DIR/versions/$version/scopelint" +if [[ ! -x "$binary" ]]; then + echo "scopelint: version $version is not installed." >&2 + echo " Run 'scopelintup --version $version' to install it." >&2 + exit 1 +fi + +exec "$binary" "$@" +SHIM +chmod +x "$SCOPELINT_BIN_DIR/scopelint" + +# 5. Add to PATH in shell profiles if not already present. +add_to_path() { + local profile="$1" + if [[ -f "$profile" ]] && grep -q 'scopelint/bin' "$profile" 2>/dev/null; then + return + fi + # Only touch files that already exist or the most common default. + [[ -f "$profile" ]] || return + echo >> "$profile" + echo '# scopelint version manager' >> "$profile" + echo 'export PATH="$HOME/.scopelint/bin:$PATH"' >> "$profile" + say "added ~/.scopelint/bin to PATH in $profile" +} + +case "$(basename "${SHELL:-bash}")" in + zsh) add_to_path "$HOME/.zshrc" ;; + bash) + # Prefer .bashrc, fall back to .bash_profile on macOS. + if [[ -f "$HOME/.bashrc" ]]; then + add_to_path "$HOME/.bashrc" + elif [[ -f "$HOME/.bash_profile" ]]; then + add_to_path "$HOME/.bash_profile" + fi + ;; + fish) + # fish uses a different mechanism; just inform the user. + say "add 'set -gx PATH \$HOME/.scopelint/bin \$PATH' to your fish config" + ;; + *) add_to_path "$HOME/.profile" ;; +esac + +# 6. Optionally install the latest scopelint. +say "" +say "scopelintup installed successfully!" +say "" +say "To install the latest scopelint, run:" +say " scopelintup" +say "" +say "If 'scopelintup' is not found, start a new terminal or run:" +say " export PATH=\"\$HOME/.scopelint/bin:\$PATH\"" diff --git a/scopelintup/scopelintup b/scopelintup/scopelintup new file mode 100755 index 0000000..0c5f330 --- /dev/null +++ b/scopelintup/scopelintup @@ -0,0 +1,363 @@ +#!/usr/bin/env bash +set -euo pipefail + +# scopelintup — version manager for scopelint +# https://github.com/ScopeLift/scopelint + +SCOPELINT_DIR="${SCOPELINT_DIR:-$HOME/.scopelint}" +SCOPELINT_BIN_DIR="$SCOPELINT_DIR/bin" +SCOPELINT_VERSIONS_DIR="$SCOPELINT_DIR/versions" +SCOPELINT_CURRENT_FILE="$SCOPELINT_DIR/.current_version" + +REPO="ScopeLift/scopelint" +GITHUB_API="https://api.github.com" +GITHUB_DOWNLOAD="https://github.com/$REPO/releases/download" +GITHUB_RAW="https://raw.githubusercontent.com/$REPO/main/scopelintup/scopelintup" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +say() { printf "scopelintup: %s\n" "$*"; } +err() { say "error: $*" >&2; exit 1; } + +need_cmd() { + command -v "$1" > /dev/null 2>&1 || err "need '$1' (command not found)" +} + +# Build curl flags, optionally adding a GitHub token for API calls. +curl_args() { + local -a args=(-fsSL) + if [[ -n "${GITHUB_TOKEN:-}" ]]; then + args+=(-H "Authorization: token $GITHUB_TOKEN") + fi + printf '%s\n' "${args[@]}" +} + +# --------------------------------------------------------------------------- +# Platform detection +# --------------------------------------------------------------------------- + +detect_platform() { + local os arch + + os="$(uname -s)" + arch="$(uname -m)" + + case "$os" in + Linux) os="linux" ;; + Darwin) os="macos" ;; + MINGW*|MSYS*|CYGWIN*) os="windows" ;; + *) err "unsupported OS: $os" ;; + esac + + case "$arch" in + x86_64|amd64) arch="x86_64" ;; + aarch64|arm64) arch="aarch64" ;; + *) err "unsupported architecture: $arch" ;; + esac + + # Archive name matches the release workflow matrix. + PLATFORM="${arch}-${os}" + if [[ "$os" == "windows" ]]; then + ARCHIVE="scopelint-${PLATFORM}.zip" + else + ARCHIVE="scopelint-${PLATFORM}.tar.xz" + fi +} + +# --------------------------------------------------------------------------- +# GitHub helpers +# --------------------------------------------------------------------------- + +# Fetch the latest release tag (e.g. v1.0.0). +latest_tag() { + need_cmd curl + local tag + tag=$(curl $(curl_args) "$GITHUB_API/repos/$REPO/releases/latest" \ + | grep '"tag_name"' | head -1 | sed 's/.*: *"\(.*\)".*/\1/') + [[ -n "$tag" ]] || err "could not determine latest release" + echo "$tag" +} + +# List all release tags from GitHub. +list_remote_tags() { + need_cmd curl + curl $(curl_args) "$GITHUB_API/repos/$REPO/releases" \ + | grep '"tag_name"' | sed 's/.*: *"\(.*\)".*/\1/' +} + +# --------------------------------------------------------------------------- +# Version helpers +# --------------------------------------------------------------------------- + +# Normalise tag: ensure it starts with 'v'. +normalise_tag() { + local tag="$1" + [[ "$tag" == v* ]] || tag="v$tag" + echo "$tag" +} + +current_version() { + if [[ -f "$SCOPELINT_CURRENT_FILE" ]]; then + cat "$SCOPELINT_CURRENT_FILE" + fi +} + +set_current_version() { + echo "$1" > "$SCOPELINT_CURRENT_FILE" +} + +# --------------------------------------------------------------------------- +# Core actions +# --------------------------------------------------------------------------- + +download_version() { + local tag="$1" + tag=$(normalise_tag "$tag") + + local dest="$SCOPELINT_VERSIONS_DIR/$tag" + if [[ -d "$dest" ]]; then + say "version $tag is already installed" + set_current_version "$tag" + return + fi + + detect_platform + + say "downloading scopelint $tag ($PLATFORM)..." + + local url="$GITHUB_DOWNLOAD/$tag/$ARCHIVE" + local tmpdir + tmpdir=$(mktemp -d) + trap 'rm -rf "${tmpdir:-}"' EXIT + + need_cmd curl + if ! curl $(curl_args) -o "$tmpdir/$ARCHIVE" "$url"; then + err "failed to download $url — does release $tag exist for $PLATFORM?" + fi + + mkdir -p "$dest" + + case "$ARCHIVE" in + *.tar.xz) + need_cmd tar + tar xJf "$tmpdir/$ARCHIVE" -C "$tmpdir" + mv "$tmpdir/scopelint-${PLATFORM}/scopelint" "$dest/scopelint" + ;; + *.zip) + need_cmd unzip + unzip -q "$tmpdir/$ARCHIVE" -d "$tmpdir" + mv "$tmpdir/scopelint-${PLATFORM}/scopelint.exe" "$dest/scopelint.exe" + ;; + esac + + chmod +x "$dest/scopelint"* 2>/dev/null || true + set_current_version "$tag" + say "installed scopelint $tag" +} + +write_shim() { + mkdir -p "$SCOPELINT_BIN_DIR" + cat > "$SCOPELINT_BIN_DIR/scopelint" <<'SHIM' +#!/usr/bin/env bash +set -euo pipefail + +SCOPELINT_DIR="${SCOPELINT_DIR:-$HOME/.scopelint}" + +# 1. Walk up looking for .scopelint-version +resolve_from_file() { + local dir="$PWD" + while [[ "$dir" != "/" ]]; do + if [[ -f "$dir/.scopelint-version" ]]; then + cat "$dir/.scopelint-version" + return + fi + dir="$(dirname "$dir")" + done +} + +version=$(resolve_from_file) + +# 2. Environment variable +if [[ -z "${version:-}" ]]; then + version="${SCOPELINT_VERSION:-}" +fi + +# 3. Global default +if [[ -z "${version:-}" && -f "$SCOPELINT_DIR/.current_version" ]]; then + version=$(cat "$SCOPELINT_DIR/.current_version") +fi + +if [[ -z "${version:-}" ]]; then + echo "scopelint: no version configured." >&2 + echo " Run 'scopelintup' to install the latest version," >&2 + echo " or create a .scopelint-version file." >&2 + exit 1 +fi + +# Normalise: ensure 'v' prefix. +[[ "$version" == v* ]] || version="v$version" + +binary="$SCOPELINT_DIR/versions/$version/scopelint" +if [[ ! -x "$binary" ]]; then + echo "scopelint: version $version is not installed." >&2 + echo " Run 'scopelintup --version $version' to install it." >&2 + exit 1 +fi + +exec "$binary" "$@" +SHIM + chmod +x "$SCOPELINT_BIN_DIR/scopelint" +} + +install_latest() { + local tag + tag=$(latest_tag) + download_version "$tag" + write_shim + say "default version set to $tag" +} + +install_version() { + local tag + tag=$(normalise_tag "$1") + download_version "$tag" + write_shim + say "default version set to $tag" +} + +use_version() { + local tag + tag=$(normalise_tag "$1") + if [[ ! -d "$SCOPELINT_VERSIONS_DIR/$tag" ]]; then + err "version $tag is not installed — run 'scopelintup --version $tag' first" + fi + set_current_version "$tag" + say "global default set to $tag" +} + +pin_version() { + local tag + tag=$(normalise_tag "$1") + echo "$tag" > .scopelint-version + say "wrote .scopelint-version ($tag)" +} + +list_installed() { + local current + current=$(current_version) + + if [[ ! -d "$SCOPELINT_VERSIONS_DIR" ]] || [[ -z "$(ls -A "$SCOPELINT_VERSIONS_DIR" 2>/dev/null)" ]]; then + say "no versions installed" + return + fi + + for dir in "$SCOPELINT_VERSIONS_DIR"/*/; do + local ver + ver=$(basename "$dir") + if [[ "$ver" == "$current" ]]; then + echo " $ver *" + else + echo " $ver" + fi + done +} + +list_remote() { + say "available releases:" + list_remote_tags | while read -r tag; do + echo " $tag" + done +} + +self_update() { + say "updating scopelintup..." + need_cmd curl + local dest="$SCOPELINT_DIR/scopelintup" + curl $(curl_args) -o "$dest" "$GITHUB_RAW" + chmod +x "$dest" + + # Refresh symlink in bin dir. + ln -sf "$dest" "$SCOPELINT_BIN_DIR/scopelintup" + say "scopelintup updated" +} + +usage() { + cat < Install a specific version (e.g. v0.1.0) + --list List installed versions + --list-remote List available versions from GitHub + --use Set the global default version + --pin Write .scopelint-version in the current directory + --self-update Update scopelintup itself + -h, --help Show this help message + +ENVIRONMENT: + SCOPELINT_VERSION Override version (used by the shim) + SCOPELINT_DIR Override base directory (default: ~/.scopelint) + GITHUB_TOKEN GitHub token to avoid API rate limits +EOF +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +main() { + # Ensure directories exist. + mkdir -p "$SCOPELINT_BIN_DIR" "$SCOPELINT_VERSIONS_DIR" + + if [[ $# -eq 0 ]]; then + install_latest + exit 0 + fi + + while [[ $# -gt 0 ]]; do + case "$1" in + --version) + [[ -n "${2:-}" ]] || err "--version requires a tag argument" + install_version "$2" + shift 2 + ;; + --list) + list_installed + shift + ;; + --list-remote) + list_remote + shift + ;; + --use) + [[ -n "${2:-}" ]] || err "--use requires a tag argument" + use_version "$2" + shift 2 + ;; + --pin) + [[ -n "${2:-}" ]] || err "--pin requires a tag argument" + pin_version "$2" + shift 2 + ;; + --self-update) + self_update + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + err "unknown option: $1 (see --help)" + ;; + esac + done +} + +main "$@" diff --git a/scopelintup/test-scopelintup.sh b/scopelintup/test-scopelintup.sh new file mode 100755 index 0000000..a2b9350 --- /dev/null +++ b/scopelintup/test-scopelintup.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +# Run scopelintup tests in an isolated temp directory. +# Usage: ./scopelintup/test-scopelintup.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCOPELINTUP="$SCRIPT_DIR/scopelintup" + +# Use a fresh temp dir each run so tests don't depend on prior state +export SCOPELINT_DIR="$(mktemp -d)" +export PATH="$SCOPELINT_DIR/bin:$PATH" + +PASS=0 +FAIL=0 + +run() { + if "$@"; then + echo " OK $*" + ((PASS++)) || true + return 0 + else + echo " FAIL $*" + ((FAIL++)) || true + return 1 + fi +} + +run_expect_fail() { + if ! "$@" 2>/dev/null; then + echo " OK (expected fail) $*" + ((PASS++)) || true + return 0 + else + echo " FAIL (expected to fail) $*" + ((FAIL++)) || true + return 1 + fi +} + +assert_output() { + local expected="$1" + shift + local out + out=$("$@") + if [[ "$out" == *"$expected"* ]]; then + echo " OK $* (output contains '$expected')" + ((PASS++)) || true + return 0 + else + echo " FAIL $* (expected output containing '$expected', got: $out)" + ((FAIL++)) || true + return 1 + fi +} + +echo "scopelintup test suite (SCOPELINT_DIR=$SCOPELINT_DIR)" +echo "" + +# --- CLI --- +echo "== CLI" +run "$SCOPELINTUP" --help +assert_output "version manager" "$SCOPELINTUP" --help +run "$SCOPELINTUP" --list +assert_output "no versions installed" "$SCOPELINTUP" --list +assert_output "available releases" "$SCOPELINTUP" --list-remote +run_expect_fail "$SCOPELINTUP" --invalid 2>/dev/null +echo "" + +# --- Install and shim --- +echo "== Install and shim" +run "$SCOPELINTUP" --version v0.0.21 +assert_output "scopelint 0.0.21" scopelint --version +assert_output "v0.0.21" "$SCOPELINTUP" --list +run "$SCOPELINTUP" --version v0.0.20 +assert_output "scopelint 0.0.20" scopelint --version +echo "" + +# --- Version resolution --- +echo "== Version resolution" +PIN_DIR=$(mktemp -d) +echo "v0.0.21" > "$PIN_DIR/.scopelint-version" +out=$(cd "$PIN_DIR" && scopelint --version) +if [[ "$out" == *"0.0.21"* ]]; then + echo " OK .scopelint-version in project selects v0.0.21" + ((PASS++)) || true +else + echo " FAIL .scopelint-version: got $out" + ((FAIL++)) || true +fi +rm -rf "$PIN_DIR" + +run "$SCOPELINTUP" --use v0.0.21 +assert_output "scopelint 0.0.21" scopelint --version + +PIN_DIR=$(mktemp -d) +(cd "$PIN_DIR" && run "$SCOPELINTUP" --pin v0.0.21) +[[ "$(cat "$PIN_DIR/.scopelint-version")" == "v0.0.21" ]] && { echo " OK --pin wrote .scopelint-version"; ((PASS++)) || true; } || { echo " FAIL --pin"; ((FAIL++)) || true; } +rm -rf "$PIN_DIR" +echo "" + +# --- Error handling --- +echo "== Error handling" +# No version configured: remove .current_version temporarily +mv "$SCOPELINT_DIR/.current_version" "$SCOPELINT_DIR/.current_version.bak" 2>/dev/null || true +out=$(cd /tmp && scopelint --version 2>&1) || true +if [[ "$out" == *"no version configured"* ]]; then + echo " OK shim reports 'no version configured' when no default" + ((PASS++)) || true +else + echo " FAIL no version: got $out" + ((FAIL++)) || true +fi +mv "$SCOPELINT_DIR/.current_version.bak" "$SCOPELINT_DIR/.current_version" 2>/dev/null || true + +out=$(SCOPELINT_VERSION=v0.0.99 scopelint --version 2>&1) || true +if [[ "$out" == *"not installed"* ]]; then + echo " OK shim reports version not installed for missing version" + ((PASS++)) || true +else + echo " FAIL not installed: got $out" + ((FAIL++)) || true +fi +echo "" + +# --- Summary --- +echo "== Summary" +echo " Passed: $PASS" +echo " Failed: $FAIL" +if [[ $FAIL -gt 0 ]]; then + echo " SCOPELINT_DIR left at $SCOPELINT_DIR for inspection" + exit 1 +fi +echo " All tests passed." +exit 0