From 9f7eeb7dc6f271ca866ba85a68baa57805f6b8ac Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 28 Aug 2026 23:46:14 -0600 Subject: [PATCH] fix: Harden install scripts: detect libssl3 without ldconfig on PATH, survive noexec tmp dirs, and guard against partial curl|bash execution --- scripts/install_coyote.ps1 | 196 ++++++++++-------- scripts/install_coyote.sh | 406 +++++++++++++++++++------------------ 2 files changed, 323 insertions(+), 279 deletions(-) diff --git a/scripts/install_coyote.ps1 b/scripts/install_coyote.ps1 index 3103789..052892a 100644 --- a/scripts/install_coyote.ps1 +++ b/scripts/install_coyote.ps1 @@ -16,6 +16,8 @@ param( [string]$BinDir = $env:BIN_DIR ) +if ($Version -and $Version -match '^[0-9]') { $Version = "v$Version" } + $Repo = 'Dark-Alex-17/coyote' function Write-Info($msg) { Write-Host "[coyote-install] $msg" } @@ -89,105 +91,127 @@ if ($os -eq 'windows') { $tmp = New-Item -ItemType Directory -Force -Path ([IO.Path]::Combine([IO.Path]::GetTempPath(), "coyote-$(Get-Random)")) -$exec = if ($isWin) { 'coyote.exe' } else { 'coyote' } -$dest = Join-Path $BinDir $exec +try { + $exec = if ($isWin) { 'coyote.exe' } else { 'coyote' } + $dest = Join-Path $BinDir $exec -$installed = $false -$tried = @() -$attempt = 0 -foreach ($c in $candidates) { - $asset = $release.assets | Where-Object { $_.name -eq $c } | Select-Object -First 1 - if (-not $asset) { - $tried += "${c}: no matching release asset" - continue - } + $installed = $false + $tried = @() + $attempt = 0 + foreach ($c in $candidates) { + $asset = $release.assets | Where-Object { $_.name -eq $c } | Select-Object -First 1 + if (-not $asset) { + $tried += "${c}: no matching release asset" + continue + } - $attempt++ - $work = New-Item -ItemType Directory -Force -Path (Join-Path $tmp.FullName "attempt-$attempt") + $attempt++ + $work = New-Item -ItemType Directory -Force -Path (Join-Path $tmp.FullName "attempt-$attempt") - Write-Info "Selected asset: $($asset.name)" - Write-Info "Download URL: $($asset.browser_download_url)" + Write-Info "Selected asset: $($asset.name)" + Write-Info "Download URL: $($asset.browser_download_url)" - $archive = Join-Path $work.FullName 'asset' - try { - Invoke-WebRequest -UseBasicParsing -Headers @{ 'User-Agent' = 'coyote-installer' } -Uri $asset.browser_download_url -OutFile $archive - } catch { - Write-Info "Failed to download ${c}; trying next candidate. $_" - $tried += "${c}: download failed" - continue - } + $archive = Join-Path $work.FullName 'asset' + try { + Invoke-WebRequest -UseBasicParsing -Headers @{ 'User-Agent' = 'coyote-installer' } -Uri $asset.browser_download_url -OutFile $archive + } catch { + Write-Info "Failed to download ${c}; trying next candidate. $_" + $tried += "${c}: download failed" + continue + } - $extractDir = Join-Path $work.FullName 'extract'; New-Item -ItemType Directory -Force -Path $extractDir | Out-Null + $extractDir = Join-Path $work.FullName 'extract'; New-Item -ItemType Directory -Force -Path $extractDir | Out-Null - try { - if ($asset.name -match '\.zip$') { - Add-Type -AssemblyName System.IO.Compression.FileSystem - [System.IO.Compression.ZipFile]::ExtractToDirectory($archive, $extractDir) - } elseif ($asset.name -match '\.tar\.gz$' -or $asset.name -match '\.tgz$') { - $tar = Get-Command tar -ErrorAction SilentlyContinue - if ($tar) { & $tar.Source -xzf $archive -C $extractDir } - else { throw "Asset is tar archive but 'tar' is not available." } - } else { - try { Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory($archive, $extractDir) } - catch { + try { + if ($asset.name -match '\.zip$') { + Add-Type -AssemblyName System.IO.Compression.FileSystem + [System.IO.Compression.ZipFile]::ExtractToDirectory($archive, $extractDir) + } elseif ($asset.name -match '\.tar\.gz$' -or $asset.name -match '\.tgz$') { $tar = Get-Command tar -ErrorAction SilentlyContinue - if ($tar) { & $tar.Source -xf $archive -C $extractDir } else { throw "Unknown archive format; neither zip nor tar workable." } + if ($tar) { & $tar.Source -xzf $archive -C $extractDir } + else { throw "Asset is tar archive but 'tar' is not available." } + } else { + try { Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory($archive, $extractDir) } + catch { + $tar = Get-Command tar -ErrorAction SilentlyContinue + if ($tar) { & $tar.Source -xf $archive -C $extractDir } else { throw "Unknown archive format; neither zip nor tar workable." } + } + } + } catch { + Write-Info "Failed to extract ${c}; trying next candidate. $_" + $tried += "${c}: extract failed" + continue + } + + $bin = $null + Get-ChildItem -Recurse -File $extractDir | ForEach-Object { + if ($isWin) { if ($_.Name -ieq 'coyote.exe') { $bin = $_.FullName } } + else { if ($_.Name -ieq 'coyote') { $bin = $_.FullName } } + } + if (-not $bin) { + Write-Info "Could not find coyote binary inside ${c}; trying next candidate" + $tried += "${c}: no coyote binary in archive" + continue + } + + if (-not $isWin) { try { & chmod +x -- $bin } catch {} } + + $works = $false + try { & $bin --version *> $null; if ($LASTEXITCODE -eq 0) { $works = $true } } catch { } + if (-not $works -and -not $isWin) { + # The temp dir may live on a noexec mount; retry from a probe file in + # the install directory before rejecting. + $probe = Join-Path $BinDir ".coyote-install-probe-$PID" + try { + Copy-Item -Force $bin $probe + & chmod +x -- $probe + & $probe --version *> $null + if ($LASTEXITCODE -eq 0) { $works = $true } + } catch { } finally { + Remove-Item -Force -ErrorAction SilentlyContinue $probe } } - } catch { - Write-Info "Failed to extract ${c}; trying next candidate. $_" - $tried += "${c}: extract failed" - continue + if (-not $works) { + Write-Info "Downloaded $c but it failed to run on this system; trying next candidate" + $tried += "${c}: binary failed to run on this system" + continue + } + + Copy-Item -Force $bin $dest + Write-Info "Installed: $dest" + $installed = $true + break } - $bin = $null - Get-ChildItem -Recurse -File $extractDir | ForEach-Object { - if ($isWin) { if ($_.Name -ieq 'coyote.exe') { $bin = $_.FullName } } - else { if ($_.Name -ieq 'coyote') { $bin = $_.FullName } } - } - if (-not $bin) { - Write-Info "Could not find coyote binary inside ${c}; trying next candidate" - $tried += "${c}: no coyote binary in archive" - continue + if (-not $installed) { + Write-Error "No usable asset found for $os-$arch. Tried:" + $tried | ForEach-Object { Write-Error " - $_" } + exit 1 } - if (-not $isWin) { try { & chmod +x -- $bin } catch {} } - - $works = $false - try { & $bin --version *> $null; if ($LASTEXITCODE -eq 0) { $works = $true } } catch { } - if (-not $works) { - Write-Info "Downloaded $c but it failed to run on this system; trying next candidate" - $tried += "${c}: binary failed to run on this system" - continue - } - - Copy-Item -Force $bin $dest - Write-Info "Installed: $dest" - $installed = $true - break -} - -if (-not $installed) { - Write-Error "No usable asset found for $os-$arch. Tried:" - $tried | ForEach-Object { Write-Error " - $_" } - exit 1 -} - -if ($isWin) { - $pathParts = ($env:Path -split ';') | Where-Object { $_ -ne '' } - if ($pathParts -notcontains $BinDir) { - $userPath = [Environment]::GetEnvironmentVariable('Path', 'User'); if (-not $userPath) { $userPath = '' } - if (-not ($userPath -split ';' | Where-Object { $_ -eq $BinDir })) { - $newUserPath = if ($userPath.Trim().Length -gt 0) { "$userPath;$BinDir" } else { $BinDir } - [Environment]::SetEnvironmentVariable('Path', $newUserPath, 'User') - Write-Info "Added to User PATH: $BinDir (restart shell to take effect)" + if ($isWin) { + $pathParts = ($env:Path -split ';') | Where-Object { $_ -ne '' } + if ($pathParts -notcontains $BinDir) { + # Read/write the User PATH via the registry directly: the [Environment] + # round-trip expands %VAR% entries and bakes them in on write. + $regKey = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true) + if ($regKey) { + $userPath = [string]$regKey.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) + if (-not (($userPath -split ';') -contains $BinDir)) { + $newUserPath = if ($userPath.Trim().Length -gt 0) { "$userPath;$BinDir" } else { $BinDir } + $regKey.SetValue('Path', $newUserPath, [Microsoft.Win32.RegistryValueKind]::ExpandString) + Write-Info "Added to User PATH: $BinDir (restart shell to take effect)" + } + $regKey.Close() + } + } + } else { + if (-not ($env:PATH -split ':' | Where-Object { $_ -eq $BinDir })) { + Write-Info "Note: $BinDir is not in PATH. Add it to your shell profile." } } -} else { - if (-not ($env:PATH -split ':' | Where-Object { $_ -eq $BinDir })) { - Write-Info "Note: $BinDir is not in PATH. Add it to your shell profile." - } + + Write-Info "Done. Try: coyote --help" +} finally { + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $tmp } - -Write-Info "Done. Try: coyote --help" - diff --git a/scripts/install_coyote.sh b/scripts/install_coyote.sh index 9692e36..58d68cf 100755 --- a/scripts/install_coyote.sh +++ b/scripts/install_coyote.sh @@ -13,8 +13,6 @@ set -euo pipefail # --bin-dir Install directory (default: /usr/local/bin or ~/.local/bin). Or set BIN_DIR. REPO="Dark-Alex-17/coyote" -VERSION="${COYOTE_VERSION:-}" -BIN_DIR="${BIN_DIR:-}" usage() { echo "coyote installer (Linux/macOS)" @@ -25,24 +23,6 @@ usage() { echo " -h, --help Show help" } -while [[ $# -gt 0 ]]; do - case "$1" in - --version) VERSION="$2"; shift 2;; - --bin-dir) BIN_DIR="$2"; shift 2;; - -h|--help) usage; exit 0;; - *) echo "Unknown argument: $1" >&2; usage; exit 2;; - esac -done - -if [[ -z "${BIN_DIR}" ]]; then - if [[ -w "/usr/local/bin" ]]; then - BIN_DIR="/usr/local/bin" - else - BIN_DIR="${HOME}/.local/bin" - fi -fi -mkdir -p "${BIN_DIR}" - log() { echo "[coyote-install] $*" } @@ -54,42 +34,6 @@ need_cmd() { fi } -need_cmd uname -need_cmd mktemp -need_cmd tar - -if command -v curl >/dev/null 2>&1; then - DL=curl -elif command -v wget >/dev/null 2>&1; then - DL=wget -else - echo "Error: need curl or wget" >&2 - exit 1 -fi - -UNAME_OS=$(uname -s | tr '[:upper:]' '[:lower:]') -case "$UNAME_OS" in - linux) OS=linux ;; - darwin) OS=darwin ;; - *) echo "Error: unsupported OS '$UNAME_OS'" >&2; exit 1;; -esac - -UNAME_ARCH=$(uname -m) -case "$UNAME_ARCH" in - x86_64|amd64) ARCH=x86_64 ;; - aarch64|arm64) ARCH=aarch64 ;; - *) echo "Error: unsupported arch '$UNAME_ARCH'" >&2; exit 1;; -esac - -log "Target: ${OS}-${ARCH}" - -API_BASE="https://api.github.com/repos/${REPO}/releases" -if [[ -z "${VERSION}" ]]; then - RELEASE_URL="${API_BASE}/latest" -else - RELEASE_URL="${API_BASE}/tags/${VERSION}" -fi - http_get() { if [[ "$DL" == "curl" ]]; then curl -fsSL -H 'User-Agent: coyote-installer' "$1" @@ -98,164 +42,240 @@ http_get() { fi } -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -log "Fetching release metadata from $RELEASE_URL" -JSON="$TMPDIR/release.json" -if ! http_get "$RELEASE_URL" > "$JSON"; then - echo "Error: failed to fetch release metadata. Check version tag." >&2 - exit 1 -fi - -ASSET_CANDIDATES=() -if [[ "$OS" == "darwin" ]]; then - if [[ "$ARCH" == "x86_64" ]]; then - ASSET_CANDIDATES+=("coyote-x86_64-apple-darwin.tar.gz") - else - ASSET_CANDIDATES+=("coyote-aarch64-apple-darwin.tar.gz") +smoke_test() { + # The scratch dir may live on a noexec mount; if running in place fails, + # retry from a probe file in the install directory before rejecting. + local bin="$1" + if "$bin" --version >/dev/null 2>&1; then return 0; fi + local probe="${BIN_DIR}/.coyote-install-probe.$$" + local ok=1 + if cp "$bin" "$probe" 2>/dev/null && chmod +x "$probe" 2>/dev/null; then + if "$probe" --version >/dev/null 2>&1; then ok=0; fi fi -elif [[ "$OS" == "linux" ]]; then - LIBC="musl" - if command -v getconf >/dev/null 2>&1 && getconf GNU_LIBC_VERSION >/dev/null 2>&1; then LIBC="gnu"; fi - if ldd --version 2>&1 | grep -qi glibc; then LIBC="gnu"; fi + rm -f "$probe" + return "$ok" +} - if [[ "$LIBC" == "gnu" ]]; then - # The gnu binary dynamically links OpenSSL 3. On Debian/Ubuntu, ldconfig lives - # in /usr/sbin, which is often missing from non-root PATHs, so try its known - # locations and fall back to probing the usual library directories directly. - LIBSSL3="" - for LDCONFIG in ldconfig /sbin/ldconfig /usr/sbin/ldconfig; do - if command -v "$LDCONFIG" >/dev/null 2>&1; then - if "$LDCONFIG" -p 2>/dev/null | grep -q 'libssl\.so\.3'; then LIBSSL3="yes"; fi +main() { + VERSION="${COYOTE_VERSION:-}" + BIN_DIR="${BIN_DIR:-}" + + while [[ $# -gt 0 ]]; do + case "$1" in + --version) VERSION="$2"; shift 2;; + --bin-dir) BIN_DIR="$2"; shift 2;; + -h|--help) usage; exit 0;; + *) echo "Unknown argument: $1" >&2; usage; exit 2;; + esac + done + + if [[ -n "$VERSION" && "$VERSION" =~ ^[0-9] ]]; then VERSION="v${VERSION}"; fi + + if [[ -z "${BIN_DIR}" ]]; then + if [[ -w "/usr/local/bin" ]]; then + BIN_DIR="/usr/local/bin" + else + BIN_DIR="${HOME}/.local/bin" + fi + fi + mkdir -p "${BIN_DIR}" + + need_cmd uname + need_cmd mktemp + need_cmd tar + + if command -v curl >/dev/null 2>&1; then + DL=curl + elif command -v wget >/dev/null 2>&1; then + DL=wget + else + echo "Error: need curl or wget" >&2 + exit 1 + fi + + UNAME_OS=$(uname -s | tr '[:upper:]' '[:lower:]') + case "$UNAME_OS" in + linux) OS=linux ;; + darwin) OS=darwin ;; + *) echo "Error: unsupported OS '$UNAME_OS'" >&2; exit 1;; + esac + + UNAME_ARCH=$(uname -m) + case "$UNAME_ARCH" in + x86_64|amd64) ARCH=x86_64 ;; + aarch64|arm64) ARCH=aarch64 ;; + *) echo "Error: unsupported arch '$UNAME_ARCH'" >&2; exit 1;; + esac + + log "Target: ${OS}-${ARCH}" + + API_BASE="https://api.github.com/repos/${REPO}/releases" + if [[ -z "${VERSION}" ]]; then + RELEASE_URL="${API_BASE}/latest" + else + RELEASE_URL="${API_BASE}/tags/${VERSION}" + fi + + WORKDIR="$(mktemp -d)" + trap 'rm -rf "$WORKDIR"; rm -f "${BIN_DIR}/.coyote-install-probe.$$"' EXIT + + log "Fetching release metadata from $RELEASE_URL" + JSON="$WORKDIR/release.json" + if ! http_get "$RELEASE_URL" > "$JSON"; then + echo "Error: failed to fetch release metadata. Check version tag." >&2 + exit 1 + fi + + ASSET_CANDIDATES=() + if [[ "$OS" == "darwin" ]]; then + if [[ "$ARCH" == "x86_64" ]]; then + ASSET_CANDIDATES+=("coyote-x86_64-apple-darwin.tar.gz") + else + ASSET_CANDIDATES+=("coyote-aarch64-apple-darwin.tar.gz") + fi + elif [[ "$OS" == "linux" ]]; then + LIBC="musl" + if command -v getconf >/dev/null 2>&1 && getconf GNU_LIBC_VERSION >/dev/null 2>&1; then LIBC="gnu"; fi + if ldd --version 2>&1 | grep -qi glibc; then LIBC="gnu"; fi + + if [[ "$LIBC" == "gnu" ]]; then + # The gnu binary dynamically links OpenSSL 3. On Debian/Ubuntu, ldconfig lives + # in /usr/sbin, which is often missing from non-root PATHs, so try its known + # locations and fall back to probing the usual library directories directly. + LIBSSL3="" + for LDCONFIG in ldconfig /sbin/ldconfig /usr/sbin/ldconfig; do + if command -v "$LDCONFIG" >/dev/null 2>&1; then + if "$LDCONFIG" -p 2>/dev/null | grep -q 'libssl\.so\.3'; then LIBSSL3="yes"; fi + break + fi + done + if [[ -z "$LIBSSL3" ]]; then + for LIBSSL_CANDIDATE in /usr/lib/*/libssl.so.3 /lib/*/libssl.so.3 /usr/lib64/libssl.so.3 /usr/lib/libssl.so.3 /usr/local/lib/libssl.so.3 /usr/local/lib/*/libssl.so.3; do + if [[ -e "$LIBSSL_CANDIDATE" ]]; then LIBSSL3="yes"; break; fi + done + fi + if [[ -n "$LIBSSL3" ]]; then + ASSET_CANDIDATES+=("coyote-${ARCH}-unknown-linux-gnu.tar.gz") + else + log "glibc detected but OpenSSL 3 (libssl.so.3) not found; using musl build" + fi + fi + + ASSET_CANDIDATES+=("coyote-${ARCH}-unknown-linux-musl.tar.gz") + else + echo "Error: unsupported OS for this installer: $OS" >&2; exit 1 + fi + + DL_URLS=$(grep -oE '"browser_download_url":[[:space:]]*"[^"]+"' "$JSON" \ + | sed -E 's/.*"browser_download_url":[[:space:]]*"//; s/"$//' \ + || true) + + INSTALLED="" + TRIED=() + ATTEMPT=0 + for candidate in "${ASSET_CANDIDATES[@]}"; do + ASSET_URL="" + while IFS= read -r url; do + [[ -z "$url" ]] && continue + if [[ "$url" == */"$candidate" ]]; then + ASSET_URL="$url" break fi - done - if [[ -z "$LIBSSL3" ]]; then - for LIBSSL_CANDIDATE in /usr/lib/*/libssl.so.3 /lib/*/libssl.so.3 /usr/lib64/libssl.so.3 /usr/lib/libssl.so.3 /usr/local/lib/libssl.so.3 /usr/local/lib/*/libssl.so.3; do - if [[ -e "$LIBSSL_CANDIDATE" ]]; then LIBSSL3="yes"; break; fi - done + done <<< "$DL_URLS" + + if [[ -z "$ASSET_URL" ]]; then + TRIED+=("$candidate: no matching release asset") + continue fi - if [[ -n "$LIBSSL3" ]]; then - ASSET_CANDIDATES+=("coyote-${ARCH}-unknown-linux-gnu.tar.gz") + + ATTEMPT=$((ATTEMPT + 1)) + WORK="$WORKDIR/attempt-$ATTEMPT" + mkdir -p "$WORK" + + log "Selected asset: $candidate" + log "Download URL: $ASSET_URL" + + ARCHIVE="$WORK/asset" + if [[ "$DL" == "curl" ]]; then + if ! curl -fL -H 'User-Agent: coyote-installer' "$ASSET_URL" -o "$ARCHIVE"; then + log "Failed to download $candidate; trying next candidate" + TRIED+=("$candidate: download failed") + continue + fi else - log "glibc detected but OpenSSL 3 (libssl.so.3) not found; using musl build" + if ! wget -q --header='User-Agent: coyote-installer' "$ASSET_URL" -O "$ARCHIVE"; then + log "Failed to download $candidate; trying next candidate" + TRIED+=("$candidate: download failed") + continue + fi fi - fi - ASSET_CANDIDATES+=("coyote-${ARCH}-unknown-linux-musl.tar.gz") -else - echo "Error: unsupported OS for this installer: $OS" >&2; exit 1 -fi + EXTRACTED_DIR="$WORK/extracted"; mkdir -p "$EXTRACTED_DIR" -DL_URLS=$(grep -oE '"browser_download_url":[[:space:]]*"[^"]+"' "$JSON" \ - | sed -E 's/.*"browser_download_url":[[:space:]]*"//; s/"$//' \ - || true) - -INSTALLED="" -TRIED=() -ATTEMPT=0 -for candidate in "${ASSET_CANDIDATES[@]}"; do - ASSET_URL="" - while IFS= read -r url; do - [[ -z "$url" ]] && continue - if [[ "$url" == */"$candidate" ]]; then - ASSET_URL="$url" - break - fi - done <<< "$DL_URLS" - - if [[ -z "$ASSET_URL" ]]; then - TRIED+=("$candidate: no matching release asset") - continue - fi - - ATTEMPT=$((ATTEMPT + 1)) - WORK="$TMPDIR/attempt-$ATTEMPT" - mkdir -p "$WORK" - - log "Selected asset: $candidate" - log "Download URL: $ASSET_URL" - - ARCHIVE="$WORK/asset" - if [[ "$DL" == "curl" ]]; then - if ! curl -fL -H 'User-Agent: coyote-installer' "$ASSET_URL" -o "$ARCHIVE"; then - log "Failed to download $candidate; trying next candidate" - TRIED+=("$candidate: download failed") - continue - fi - else - if ! wget -q --header='User-Agent: coyote-installer' "$ASSET_URL" -O "$ARCHIVE"; then - log "Failed to download $candidate; trying next candidate" - TRIED+=("$candidate: download failed") - continue - fi - fi - - EXTRACTED_DIR="$WORK/extracted"; mkdir -p "$EXTRACTED_DIR" - - if tar -tf "$ARCHIVE" >/dev/null 2>&1; then - if ! tar -xzf "$ARCHIVE" -C "$EXTRACTED_DIR"; then - log "Failed to extract $candidate; trying next candidate" - TRIED+=("$candidate: extract failed") - continue - fi - else - if command -v unzip >/dev/null 2>&1; then - if ! unzip -q "$ARCHIVE" -d "$EXTRACTED_DIR"; then + if tar -tf "$ARCHIVE" >/dev/null 2>&1; then + if ! tar -xzf "$ARCHIVE" -C "$EXTRACTED_DIR"; then log "Failed to extract $candidate; trying next candidate" TRIED+=("$candidate: extract failed") continue fi else - log "Unknown archive format for $candidate and 'unzip' is not available; trying next candidate" - TRIED+=("$candidate: unknown archive format and 'unzip' unavailable") + if command -v unzip >/dev/null 2>&1; then + if ! unzip -q "$ARCHIVE" -d "$EXTRACTED_DIR"; then + log "Failed to extract $candidate; trying next candidate" + TRIED+=("$candidate: extract failed") + continue + fi + else + log "Unknown archive format for $candidate and 'unzip' is not available; trying next candidate" + TRIED+=("$candidate: unknown archive format and 'unzip' unavailable") + continue + fi + fi + + BIN_PATH="" + while IFS= read -r -d '' f; do + base=$(basename "$f") + if [[ "$base" == "coyote" ]]; then + BIN_PATH="$f" + break + fi + done < <(find "$EXTRACTED_DIR" -type f -print0) + + if [[ -z "$BIN_PATH" ]]; then + log "Could not find 'coyote' binary in $candidate; trying next candidate" + TRIED+=("$candidate: no 'coyote' binary in archive") continue fi - fi - BIN_PATH="" - while IFS= read -r -d '' f; do - base=$(basename "$f") - if [[ "$base" == "coyote" ]]; then - BIN_PATH="$f" - break + chmod +x "$BIN_PATH" + if ! smoke_test "$BIN_PATH"; then + log "Downloaded $candidate but it failed to run on this system; trying next candidate" + TRIED+=("$candidate: binary failed to run on this system") + continue fi - done < <(find "$EXTRACTED_DIR" -type f -print0) - if [[ -z "$BIN_PATH" ]]; then - log "Could not find 'coyote' binary in $candidate; trying next candidate" - TRIED+=("$candidate: no 'coyote' binary in archive") - continue + install -m 0755 "$BIN_PATH" "${BIN_DIR}/coyote" + INSTALLED="$candidate" + break + done + + if [[ -z "$INSTALLED" ]]; then + echo "Error: no usable asset found for ${OS}-${ARCH}. Tried:" >&2 + for t in "${TRIED[@]}"; do echo " - $t" >&2; done + exit 1 fi - chmod +x "$BIN_PATH" - if ! "$BIN_PATH" --version >/dev/null 2>&1; then - log "Downloaded $candidate but it failed to run on this system; trying next candidate" - TRIED+=("$candidate: binary failed to run on this system") - continue - fi + log "Installed: ${BIN_DIR}/coyote" - install -m 0755 "$BIN_PATH" "${BIN_DIR}/coyote" - INSTALLED="$candidate" - break -done + case ":$PATH:" in + *":${BIN_DIR}:"*) ;; + *) + log "Note: ${BIN_DIR} is not in PATH. Add it, e.g.:" + log " export PATH=\"${BIN_DIR}:\$PATH\"" + ;; + esac -if [[ -z "$INSTALLED" ]]; then - echo "Error: no usable asset found for ${OS}-${ARCH}. Tried:" >&2 - for t in "${TRIED[@]}"; do echo " - $t" >&2; done - exit 1 -fi - -log "Installed: ${BIN_DIR}/coyote" - -case ":$PATH:" in - *":${BIN_DIR}:"*) ;; - *) - log "Note: ${BIN_DIR} is not in PATH. Add it, e.g.:" - log " export PATH=\"${BIN_DIR}:\$PATH\"" - ;; -esac - -log "Done. Try: coyote --help" + log "Done. Try: coyote --help" +} +main "$@"