4 Commits

Author SHA1 Message Date
Jason Swank
b3f4f1bfeb Update README 2026-01-25 12:05:25 -05:00
Jason Swank
9052e555b8 tenv: fix atmos 2026-01-23 16:19:17 -05:00
ffd982948a add tenv (#6) 2026-01-23 21:08:12 +00:00
106f00e062 aichat: add checksums (#5) 2026-01-23 15:14:12 +00:00
5 changed files with 1074 additions and 87 deletions

209
README.md
View File

@@ -1,104 +1,177 @@
# binst
A collection of installation scripts for binary tools, generated using [binstaller](https://github.com/binary-install/binstaller). These scripts are portable POSIX shell scripts that work across Linux, macOS, and Windows (Git Bash/WSL) and are designed to be a simple, secure way to install self-contained binaries from GitHub releases.
A curated collection of portable installation scripts for binary tools, powered by [binstaller](https://github.com/binary-install/binstaller).
* [Quick Start](#quick-start)
* [Workflows](#workflows)
* [Creating New Installation Scripts](#creating-new-installation-scripts)
* [Notes](#notes)
* [Binary Names](#binary-names)
## Overview
This repository provides POSIX-compliant shell scripts that simplify the installation of popular binary tools from GitHub releases. Each script is:
- **Portable**: Works across Linux, macOS, and Windows (Git Bash/WSL)
- **Secure**: Includes checksum verification for downloaded binaries
- **Simple**: Single command installation with no dependencies
- **Self-contained**: No package manager or runtime required
## Table of Contents
- [Quick Start](#quick-start)
- [Available Tools](#available-tools)
- [Usage](#usage)
- [Installing Binaries](#installing-binaries)
- [Using Task Commands](#using-task-commands)
- [Creating New Scripts](#creating-new-scripts)
- [Prerequisites](#prerequisites)
- [Automatic Generation](#automatic-generation)
- [Manual Configuration](#manual-configuration)
- [Configuration](#configuration)
- [Binary Names](#binary-names)
- [Contributing](#contributing)
- [License](#license)
## Quick Start
Install any binary using its generated script:
Install any binary directly using its installation script:
```bash
# install trufflehog
$ scripts/trufflehog-install.sh
# Install trufflehog
./scripts/trufflehog-install.sh
# install task
$ scripts/task-install.sh
# Install task
./scripts/task-install.sh
# Install checkov
./scripts/checkov-install.sh
```
## Workflows
Binaries are installed to `~/.local/bin` by default (customizable via environment variables).
Common workflows for the creation, maintenance, usage of the installation scripts are encapsulated in [Task](https://taskfile.dev) tasks.
## Available Tools
Installation scripts are located in the `scripts/` directory. Run `ls scripts/` to see all available tools, or browse the directory on GitHub.
## Usage
### Installing Binaries
Run any installation script directly:
```bash
# list available tasks
$ task --list
task: Available tasks for this project:
* default: Create a new installation script for a binary
* embed-checksums: Embed checksums into a binst configuration file.
* gen: Generate installation script from binst configuration.
* init: Initialize binst configuration for a GitHub project.
* install-*: Install a binary using its installation script.
* latest-release: Determine the latest release available for a given repo.
# install a binary
$ task install-trufflehog
./scripts/<tool-name>-install.sh
```
### Creating New Installation Scripts
The script will:
1. Detect your OS and architecture
2. Download the latest release from GitHub
3. Verify checksums
4. Install the binary to your PATH
The default task uses binstaller to creates a new installation script for a binary from
its GitHub repository. This is often the only step needed to add a new installation
script to the collection.
### Using Task Commands
This repository includes [Taskfile](https://taskfile.dev) automation for common workflows:
To install binstaller, run:
```bash
$ scripts/binstaller-install.sh
# List all available tasks
task --list
# Install a specific binary via Task
task install-trufflehog
task install-aichat
task install-checkov
```
To create a new installation script:
Available tasks:
- `task` - Create a new installation script (requires `REPO` variable)
- `task install-*` - Install a binary using its script
- `task init` - Initialize binstaller configuration for a GitHub project
- `task embed-checksums` - Embed checksums into a configuration file
- `task gen` - Generate installation script from configuration
- `task latest-release` - Check the latest release version
## Creating New Scripts
### Prerequisites
First, install binstaller:
```bash
# create a new installation script by providing the GitHub repository as an argument:
$ task REPO=owner/repo-name
# detailes summary
$ task --summary
task: default
Create a new installation script for a binary by initializing a binst config,
embedding checksums, and generating the installation script. The latest release, as
determined by the latest-release task, will be used unless a specific version is
provided.
Invoke this task like:
task default REPO=trufflesecurity/trufflehog
vars:
CONFIG_DIR: "./config"
SCRIPT_DIR: "./scripts"
BINARY: "{{.REPO | base}}"
VERSION: "latest"
requires:
vars:
- REPO
commands:
- Task: init
- Task: embed-checksums
- Task: gen
./scripts/binstaller-install.sh
```
## Notes
### Automatic Generation
The easiest way to add a new installation script is using the default Task workflow:
```bash
# Create script for a GitHub repository
task REPO=owner/repo-name
# Example: Add installation script for trufflehog
task REPO=trufflesecurity/trufflehog
```
This command will:
1. Initialize a binstaller configuration file
2. Fetch and embed checksums from the latest release
3. Generate the installation script
For more details on the default task:
```bash
task --summary
```
### Manual Configuration
For advanced use cases, you can manually create or edit configuration files:
1. **Initialize configuration**:
```bash
task init REPO=owner/repo-name
```
2. **Edit the configuration** in `config/repo-name.binstaller.yml` as needed
3. **Embed checksums**:
```bash
task embed-checksums BINARY=repo-name
```
4. **Generate the script**:
```bash
task gen BINARY=repo-name
```
## Configuration
### Binary Names
Sometimes the binary executable name differs from the GitHub repository name. When this occurs, the `asset.binaries` section in the configuration file defines the actual binary name. If this is the case, the generated installation script will need to be manually edited.
In some cases, the binary executable name differs from the GitHub repository name. When this occurs, specify the actual binary name in the `asset.binaries` section of the configuration file.
As an example, the released artifact for [stacklok/toolhive](https://github.com/stacklok/toolhive) is named `thv` rather than `toolhive`. The [configuration file](config/toolhive.binstaller.yml) specifies the correct name for the binary in the `asset.binaries` section.
**Example**: The [stacklok/toolhive](https://github.com/stacklok/toolhive) repository releases a binary named `thv`. The [configuration file](config/toolhive.binstaller.yml) handles this:
```yaml
repo: stacklok/toolhive
asset:
binaries:
- name: thv
path: thv
binaries:
- name: thv
path: thv
```
After generation, you may wish to rename the installation script from `thv-install.sh` to `toolhive-install.sh` for consistency.
## Contributing
Contributions are welcome! To add a new binary installation script:
1. Fork this repository
2. Create a new script using `task REPO=owner/repo-name`
3. Test the installation script
4. Submit a pull request
Please ensure:
- The binary is a popular, well-maintained tool
- The installation script works across all supported platforms
- Checksums are embedded for security
## License
MIT. See [LICENSE](LICENSE) for details.

View File

@@ -5,21 +5,37 @@ asset:
template: aichat-${TAG}-${ARCH}-${OS}${EXT}
default_extension: .tar.gz
rules:
- when:
arch: amd64
arch: x86_64
- when:
arch: arm64
arch: aarch64
- when:
os: darwin
os: apple-darwin
- when:
os: linux
os: unknown-linux-musl
- when:
os: windows
os: pc-windows-msvc
- when:
os: windows
ext: .zip
- when:
arch: amd64
arch: x86_64
- when:
arch: arm64
arch: aarch64
- when:
os: darwin
os: apple-darwin
- when:
os: linux
os: unknown-linux-musl
- when:
os: windows
os: pc-windows-msvc
- when:
os: windows
ext: .zip
checksums:
algorithm: sha256
embedded_checksums:
v0.30.0:
- filename: aichat-v0.30.0-aarch64-apple-darwin.tar.gz
hash: 680d36ed7f7ba8a2c2490a099df26c9da54495903c27956109a89c87eeb9edb3
- filename: aichat-v0.30.0-aarch64-pc-windows-msvc.zip
hash: 2f381e70ddaf0831df7e0181a1d25d700bb0d7e53287393a47fd435c253d6091
- filename: aichat-v0.30.0-aarch64-unknown-linux-musl.tar.gz
hash: eb1cd0948569404c5d9d01c10b32b902e11f8231073315456454dec246bdf26e
- filename: aichat-v0.30.0-x86_64-apple-darwin.tar.gz
hash: 94aa09315773929e027bcb399f78661c2ddffd887b65348bae446601989d9ddd
- filename: aichat-v0.30.0-x86_64-pc-windows-msvc.zip
hash: 89df8c4ccd6dad310ac713eb5d550494ae7fa14722cde346588fab38ff4afd8f
- filename: aichat-v0.30.0-x86_64-unknown-linux-musl.tar.gz
hash: 6b0cc08c5ceb551dc52bfac2221752f82215be5908c70605d655e9b91ab1557c

View File

@@ -0,0 +1,58 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/binary-install/binstaller/main/schema/InstallSpec.json
schema: v1
repo: tofuutils/tenv
asset:
template: tenv_${TAG}_${OS}_${ARCH}${EXT}
default_extension: .tar.gz
binaries:
- name: atmos
path: atmos
- name: tenv
path: tenv
- name: terraform
path: terraform
- name: terragrunt
path: terragrunt
- name: terramate
path: terramate
- name: tf
path: tf
- name: tofu
path: tofu
rules:
- when:
arch: amd64
arch: x86_64
- when:
os: darwin
os: Darwin
- when:
os: linux
os: Linux
- when:
os: windows
os: Windows
- when:
os: windows
ext: .zip
checksums:
algorithm: sha256
template: tenv_${TAG}_checksums.txt
embedded_checksums:
v4.9.0:
- filename: tenv_v4.9.0_Darwin_arm64.tar.gz
hash: 9fe1bea568d89c877198caa3af8cdf78c44263aedd6117feb783be4f242bf09b
- filename: tenv_v4.9.0_Darwin_x86_64.tar.gz
hash: d2b807d5885222745e64421911ffc35dcc15a3e9e77820fe73d0508a26cda726
- filename: tenv_v4.9.0_Linux_arm64.tar.gz
hash: 7b1e3e46e946f6ae60fb5b8a307b6481771708b6a571d0ca42e106bb490cb062
- filename: tenv_v4.9.0_Linux_armv6.tar.gz
hash: 86ca60fb1020d17d73217a0f94233a4931205e7225c11ebbfcbfb62f05d3ee85
- filename: tenv_v4.9.0_Linux_x86_64.tar.gz
hash: 3aa5bbd0147405518d29eca0468c7ea9a93326a5fbef758799caece8428442a6
- filename: tenv_v4.9.0_Windows_arm64.zip
hash: 360cba142fdd2a731ac895b87b32e62724e41bf5afc6aed0459bf1f9d4de6c63
- filename: tenv_v4.9.0_Windows_armv6.zip
hash: 1d751bc4e0abdfb30bffed4bd2c8615ab149632ce595af471e971204b769391c
- filename: tenv_v4.9.0_Windows_x86_64.zip
hash: 2b798e4155478425be2c07a8ec857dd31c66fe418f20f8fc9cac4b4a5cf91fe2

View File

@@ -409,7 +409,13 @@ github_release() {
}
# --- Embedded Checksums (Format: VERSION:FILENAME:HASH) ---
EMBEDDED_CHECKSUMS=""
EMBEDDED_CHECKSUMS="
0.30.0:aichat-v0.30.0-aarch64-apple-darwin.tar.gz:680d36ed7f7ba8a2c2490a099df26c9da54495903c27956109a89c87eeb9edb3
0.30.0:aichat-v0.30.0-aarch64-pc-windows-msvc.zip:2f381e70ddaf0831df7e0181a1d25d700bb0d7e53287393a47fd435c253d6091
0.30.0:aichat-v0.30.0-aarch64-unknown-linux-musl.tar.gz:eb1cd0948569404c5d9d01c10b32b902e11f8231073315456454dec246bdf26e
0.30.0:aichat-v0.30.0-x86_64-apple-darwin.tar.gz:94aa09315773929e027bcb399f78661c2ddffd887b65348bae446601989d9ddd
0.30.0:aichat-v0.30.0-x86_64-pc-windows-msvc.zip:89df8c4ccd6dad310ac713eb5d550494ae7fa14722cde346588fab38ff4afd8f
0.30.0:aichat-v0.30.0-x86_64-unknown-linux-musl.tar.gz:6b0cc08c5ceb551dc52bfac2221752f82215be5908c70605d655e9b91ab1557c"
# Find embedded checksum for a given version and filename
find_embedded_checksum() {

834
scripts/tenv-install.sh Executable file
View File

@@ -0,0 +1,834 @@
#!/bin/sh
# Code generated by binstaller. DO NOT EDIT.
#
set -e
usage() {
this=$1
cat <<EOF
$this: download ${NAME} from ${REPO}
Usage: $this [-b bindir] [-d] [-q] [-n] [tag]
-b sets bindir or installation directory, Defaults to ${BINSTALLER_BIN:-${HOME}/.local/bin}
-d turns on debug logging
-q turns on quiet mode (errors only)
-n turns on dry run mode
[tag] is a tag from
https://github.com/tofuutils/tenv/releases
If tag is missing, then latest will be used.
Environment variables:
BINSTALLER_NO_PROGRESS=1 Disable progress indicators
BINSTALLER_OS=... Override OS detection
BINSTALLER_ARCH=... Override architecture detection
Generated by binstaller
https://github.com/binary-install/binstaller
EOF
exit 2
}
cat /dev/null <<EOF
------------------------------------------------------------------------
https://github.com/client9/shlib - portable posix shell functions
Public domain - http://unlicense.org
https://github.com/client9/shlib/blob/master/LICENSE.md
but credit (and pull requests) appreciated.
------------------------------------------------------------------------
EOF
is_command() {
command -v "$1" >/dev/null
}
echoerr() {
echo "$@" 1>&2
}
_logp=6
log_set_priority() {
_logp="$1"
}
log_priority() {
if test -z "$1"; then
echo "$_logp"
return
fi
[ "$1" -le "$_logp" ]
}
log_tag() {
case $1 in
0) echo "emerg" ;;
1) echo "alert" ;;
2) echo "crit" ;;
3) echo "err" ;;
4) echo "warning" ;;
5) echo "notice" ;;
6) echo "info" ;;
7) echo "debug" ;;
*) echo "$1" ;;
esac
}
log_debug() {
log_priority 7 || return 0
echoerr "$(log_prefix)" "$(log_tag 7)" "$@"
}
log_info() {
log_priority 6 || return 0
echoerr "$(log_prefix)" "$(log_tag 6)" "$@"
}
log_err() {
log_priority 3 || return 0
echoerr "$(log_prefix)" "$(log_tag 3)" "$@"
}
log_crit() {
log_priority 2 || return 0
echoerr "$(log_prefix)" "$(log_tag 2)" "$@"
}
uname_os() {
os=$(uname -s | tr '[:upper:]' '[:lower:]')
case "$os" in
msys*) os="windows" ;;
mingw*) os="windows" ;;
cygwin*) os="windows" ;;
esac
if [ "$os" = "sunos" ]; then
if [ "$(uname -o)" = "illumos" ]; then
os="illumos"
else
os="solaris"
fi
fi
echo "$os"
}
uname_arch() {
arch=$(uname -m)
case $arch in
x86_64) arch="amd64" ;;
i86pc) arch="amd64" ;;
x86) arch="386" ;;
i686) arch="386" ;;
i386) arch="386" ;;
aarch64) arch="arm64" ;;
armv5*) arch="armv5" ;;
armv6*) arch="armv6" ;;
armv7*) arch="armv7" ;;
esac
echo "${arch}"
}
uname_os_check() {
os=$(uname_os)
case "$os" in
darwin) return 0 ;;
dragonfly) return 0 ;;
freebsd) return 0 ;;
linux) return 0 ;;
android) return 0 ;;
midnightbsd) return 0 ;;
nacl) return 0 ;;
netbsd) return 0 ;;
openbsd) return 0 ;;
plan9) return 0 ;;
solaris) return 0 ;;
illumos) return 0 ;;
windows) return 0 ;;
esac
log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib"
return 1
}
uname_arch_check() {
arch=$(uname_arch)
case "$arch" in
386) return 0 ;;
amd64) return 0 ;;
arm64) return 0 ;;
armv5) return 0 ;;
armv6) return 0 ;;
armv7) return 0 ;;
ppc64) return 0 ;;
ppc64le) return 0 ;;
mips) return 0 ;;
mipsle) return 0 ;;
mips64) return 0 ;;
mips64le) return 0 ;;
s390x) return 0 ;;
amd64p32) return 0 ;;
esac
log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib"
return 1
}
cat /dev/null <<EOF
------------------------------------------------------------------------
End of functions from https://github.com/client9/shlib
------------------------------------------------------------------------
EOF
hash_sha256() {
TARGET=${1:-/dev/stdin}
if is_command gsha256sum; then
hash=$(gsha256sum "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command sha256sum; then
hash=$(sha256sum "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command shasum; then
hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command openssl; then
hash=$(openssl dgst -sha256 "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f 2
else
log_crit "hash_sha256 unable to find command to compute sha-256 hash"
return 1
fi
}
hash_compute() {
hash_sha256 "$1"
}
# shellcheck shell=sh
# Terminal progress reporting functions
progress_init() {
# Only show progress on interactive terminals and when not disabled
if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then
return 0
fi
# OSC 9;4 sequences are safely ignored by unsupporting terminals
# Only need special handling for tmux passthrough
if [ -n "$TMUX" ]; then
# Tmux passthrough: DCS tmux; <doubled ESC sequence> ST
# ESC characters in the wrapped sequence must be doubled
# Format: ESC P tmux; ESC ESC ] 9;4; ... ESC ESC \ ESC \
PROGRESS_START=$(printf '\033Ptmux;\033\033]9;4;')
# shellcheck disable=SC1003
PROGRESS_END=$(printf '\033\033\\\033\\')
else
# Direct OSC 9;4 - terminals that don't support it will safely ignore
PROGRESS_START=$(printf '\033]9;4;')
PROGRESS_END=$(printf '\007')
fi
}
# Start pulsing progress animation
progress_pulse_start() {
# Only show progress on interactive terminals and when not disabled
if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then
return 0
fi
# Send OSC 9;4 with state 3 (indeterminate/pulsing) once
# The terminal will handle the continuous animation
printf "%s3%s" "$PROGRESS_START" "$PROGRESS_END" >&2
}
# Clear progress indicator
progress_clear() {
# Only show progress on interactive terminals and when not disabled
if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then
return 0
fi
printf "%s0;%s" "$PROGRESS_START" "$PROGRESS_END" >&2
}
untar() {
tarball=$1
strip_components=${2:-0} # default 0
case "${tarball}" in
*.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" --strip-components "${strip_components}" ;;
*.tar.xz) tar --no-same-owner -xJf "${tarball}" --strip-components "${strip_components}" ;;
*.tar.bz2) tar --no-same-owner -xjf "${tarball}" --strip-components "${strip_components}" ;;
*.tar) tar --no-same-owner -xf "${tarball}" --strip-components "${strip_components}" ;;
*.gz) gunzip "${tarball}" ;;
*.zip)
# unzip doesn't have a standard --strip-components
# Workaround: extract to a subdir and move contents up if stripping
if [ "$strip_components" -gt 0 ]; then
extract_dir=$(basename "${tarball%.zip}")_extracted
unzip -q "${tarball}" -d "${extract_dir}"
# Move contents of the *first* directory found inside extract_dir up
# This assumes wrap_in_directory=true convention
first_subdir=$(find "${extract_dir}" -mindepth 1 -maxdepth 1 -type d -print -quit)
if [ -n "$first_subdir" ]; then
# Move all contents (* includes hidden files)
mv "${first_subdir}"/* .
# Optionally remove the now-empty subdir and the extract_dir
rmdir "${first_subdir}"
rmdir "${extract_dir}"
else
log_warn "Could not find subdirectory in zip to strip components from ${extract_dir}"
# Files are extracted in current dir anyway, proceed
fi
else
unzip -q "${tarball}"
fi
;;
*)
log_err "untar unknown archive format for ${tarball}"
return 1
;;
esac
}
hash_verify() {
TARGET_PATH=$1
SUMFILE=$2
if [ -z "${SUMFILE}" ]; then
log_err "hash_verify checksum file not specified in arg2"
return 1
fi
got=$(hash_compute "$TARGET_PATH")
if [ -z "${got}" ]; then
log_err "failed to calculate hash: ${TARGET_PATH}"
return 1
fi
BASENAME=${TARGET_PATH##*/}
# Check for line matches in checksum file
# Format: "<hash> <filename>" or "<hash> *<filename>"
# Filename may include path prefix (e.g., "deployment/m2/file.tar.gz")
while IFS= read -r line || [ -n "$line" ]; do
# Normalize tabs to spaces
line=$(echo "$line" | tr '\t' ' ')
# Remove trailing spaces for hash-only line check
line_trimmed=$(echo "$line" | sed 's/[[:space:]]*$//')
# Check for hash-only line (no filename) - early return
if [ "$line_trimmed" = "$got" ]; then
return 0
fi
# Extract hash and filename parts
# First field is the hash, rest is filename (which may contain spaces)
line_hash=$(echo "$line" | cut -d' ' -f1)
# Skip if hash doesn't match
if [ "$line_hash" != "$got" ]; then
continue
fi
# Hash matches, now check filename
# Remove the hash part from the beginning of the line
line_rest="${line#"$got"}"
# Remove leading spaces
while [ "${line_rest#[ ]}" != "$line_rest" ]; do
line_rest="${line_rest#[ ]}"
done
# Remove leading asterisk if present (binary mode indicator)
if [ "${line_rest#\*}" != "$line_rest" ]; then
line_rest="${line_rest#\*}"
fi
# Extract just the filename without any path
line_filename="${line_rest##*/}"
# Check if the filename matches
if [ "$line_filename" = "$BASENAME" ]; then
return 0
fi
done < "$SUMFILE"
log_err "hash_verify checksum for '$TARGET_PATH' did not verify"
log_err " Expected hash: ${got}"
log_err " Checksum file content:"
cat "$SUMFILE" >&2
return 1
}
# GitHub HTTP download functions with GITHUB_TOKEN support
github_http_download_curl() {
local_file=$1
source_url=$2
header=$3
if [ -n "$GITHUB_TOKEN" ]; then
log_debug "Using GITHUB_TOKEN for authentication"
if [ -z "$header" ]; then
curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" -o "$local_file" "$source_url"
else
curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" -H "$header" -o "$local_file" "$source_url"
fi
else
if [ -z "$header" ]; then
curl -fsSL -o "$local_file" "$source_url"
else
curl -fsSL -H "$header" -o "$local_file" "$source_url"
fi
fi
}
github_http_download_wget() {
local_file=$1
source_url=$2
header=$3
if [ -n "$GITHUB_TOKEN" ]; then
log_debug "Using GITHUB_TOKEN for authentication"
if [ -z "$header" ]; then
wget -q --header "Authorization: Bearer $GITHUB_TOKEN" -O "$local_file" "$source_url"
else
wget -q --header "Authorization: Bearer $GITHUB_TOKEN" --header "$header" -O "$local_file" "$source_url"
fi
else
if [ -z "$header" ]; then
wget -q -O "$local_file" "$source_url"
else
wget -q --header "$header" -O "$local_file" "$source_url"
fi
fi
}
github_http_download() {
log_debug "github_http_download $2"
if is_command curl; then
github_http_download_curl "$@"
return
elif is_command wget; then
github_http_download_wget "$@"
return
fi
log_crit "github_http_download unable to find wget or curl"
return 1
}
github_http_copy() {
tmp=$(mktemp)
github_http_download "${tmp}" "$@" || return 1
body=$(cat "$tmp")
rm -f "${tmp}"
echo "$body"
}
github_release() {
owner_repo=$1
version=$2
test -z "$version" && version="latest"
giturl="https://github.com/${owner_repo}/releases/${version}"
json=$(github_http_copy "$giturl" "Accept:application/json")
test -z "$json" && return 1
version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//')
test -z "$version" && return 1
echo "$version"
}
# --- Embedded Checksums (Format: VERSION:FILENAME:HASH) ---
EMBEDDED_CHECKSUMS="
4.9.0:tenv_v4.9.0_Darwin_arm64.tar.gz:9fe1bea568d89c877198caa3af8cdf78c44263aedd6117feb783be4f242bf09b
4.9.0:tenv_v4.9.0_Darwin_x86_64.tar.gz:d2b807d5885222745e64421911ffc35dcc15a3e9e77820fe73d0508a26cda726
4.9.0:tenv_v4.9.0_Linux_arm64.tar.gz:7b1e3e46e946f6ae60fb5b8a307b6481771708b6a571d0ca42e106bb490cb062
4.9.0:tenv_v4.9.0_Linux_armv6.tar.gz:86ca60fb1020d17d73217a0f94233a4931205e7225c11ebbfcbfb62f05d3ee85
4.9.0:tenv_v4.9.0_Linux_x86_64.tar.gz:3aa5bbd0147405518d29eca0468c7ea9a93326a5fbef758799caece8428442a6
4.9.0:tenv_v4.9.0_Windows_arm64.zip:360cba142fdd2a731ac895b87b32e62724e41bf5afc6aed0459bf1f9d4de6c63
4.9.0:tenv_v4.9.0_Windows_armv6.zip:1d751bc4e0abdfb30bffed4bd2c8615ab149632ce595af471e971204b769391c
4.9.0:tenv_v4.9.0_Windows_x86_64.zip:2b798e4155478425be2c07a8ec857dd31c66fe418f20f8fc9cac4b4a5cf91fe2"
# Find embedded checksum for a given version and filename
find_embedded_checksum() {
version="$1"
filename="$2"
echo "$EMBEDDED_CHECKSUMS" | grep -E "^${version}:${filename}:" | cut -d':' -f3
}
parse_args() {
BINDIR="${BINSTALLER_BIN:-${HOME}/.local/bin}"
DRY_RUN=0
while getopts "b:dqh?xn" arg; do
case "$arg" in
b) BINDIR="$OPTARG" ;;
d) log_set_priority 10 ;;
q) log_set_priority 3 ;;
h | \?) usage "$0" ;;
x) set -x ;;
n) DRY_RUN=1 ;;
esac
done
shift $((OPTIND - 1))
TAG="${1:-latest}"
}
tag_to_version() {
if [ "$TAG" = "latest" ]; then
log_info "checking GitHub for latest tag"
REALTAG=$(github_release "${REPO}" "${TAG}") && true
test -n "$REALTAG" || {
log_crit "Could not determine latest tag for ${REPO}"
exit 1
}
else
# Assume TAG is a valid tag/version string
REALTAG="$TAG"
fi
if test -z "$REALTAG"; then
log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${REPO}/releases for details"
exit 1
fi
VERSION=${REALTAG#v} # Strip leading 'v'
TAG="$REALTAG" # Use the resolved tag
log_info "Resolved version: ${VERSION} (tag: ${TAG})"
}
resolve_asset_filename() {
# --- Apply Rules ---
ASSET_FILENAME=""
if [ "${UNAME_ARCH}" = 'amd64' ] && true
then
ARCH='x86_64'
fi
if [ "${UNAME_OS}" = 'darwin' ] && true
then
OS='Darwin'
fi
if [ "${UNAME_OS}" = 'linux' ] && true
then
OS='Linux'
fi
if [ "${UNAME_OS}" = 'windows' ] && true
then
OS='Windows'
fi
if [ "${UNAME_OS}" = 'windows' ] && true
then
EXT='.zip'
fi
if [ -z "${ASSET_FILENAME}" ]; then
ASSET_FILENAME="tenv_${TAG}_${OS}_${ARCH}${EXT}"
fi
}
# Cleanup function to remove temporary files and stop progress
cleanup() {
# Stop progress animation
progress_clear
if [ -n "$TMPDIR" ] && [ -d "$TMPDIR" ]; then
log_debug "Cleaning up temporary directory: $TMPDIR"
rm -rf -- "$TMPDIR"
fi
}
execute() {
STRIP_COMPONENTS=0
CHECKSUM_FILENAME="tenv_${TAG}_checksums.txt"
# --- Construct URLs ---
GITHUB_DOWNLOAD="https://github.com/${REPO}/releases/download"
ASSET_URL="${GITHUB_DOWNLOAD}/${TAG}/${ASSET_FILENAME}"
CHECKSUM_URL=""
if [ -n "$CHECKSUM_FILENAME" ]; then
CHECKSUM_URL="${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM_FILENAME}"
fi
# --- Download and Verify ---
TMPDIR=$(mktemp -d)
# Set up cleanup trap that includes progress clearing
trap cleanup EXIT HUP INT TERM
log_debug "Downloading files into ${TMPDIR}"
log_info "Downloading ${ASSET_URL}"
github_http_download "${TMPDIR}/${ASSET_FILENAME}" "${ASSET_URL}"
# Try to find embedded checksum first
EMBEDDED_HASH=$(find_embedded_checksum "$VERSION" "$ASSET_FILENAME")
if [ -n "$EMBEDDED_HASH" ]; then
log_info "Using embedded checksum for verification"
# Verify using embedded hash
got=$(hash_compute "${TMPDIR}/${ASSET_FILENAME}")
if [ "$got" != "$EMBEDDED_HASH" ]; then
log_crit "Checksum verification failed for ${ASSET_FILENAME}"
log_crit "Expected: ${EMBEDDED_HASH}"
log_crit "Got: ${got}"
return 1
fi
log_info "Checksum verification successful"
elif [ -n "$CHECKSUM_URL" ]; then
# Fall back to downloading checksum file
log_info "Downloading checksums from ${CHECKSUM_URL}"
github_http_download "${TMPDIR}/${CHECKSUM_FILENAME}" "${CHECKSUM_URL}"
log_info "Verifying checksum ..."
hash_verify "${TMPDIR}/${ASSET_FILENAME}" "${TMPDIR}/${CHECKSUM_FILENAME}"
else
log_info "No checksum found, skipping verification."
fi
if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then
log_debug "Target is raw binary"
else
log_info "Extracting ${ASSET_FILENAME}..."
(cd "${TMPDIR}" && untar "${ASSET_FILENAME}" "${STRIP_COMPONENTS}")
fi
BINARY_NAME='atmos'
if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then
BINARY_PATH="${TMPDIR}/${ASSET_FILENAME}"
else
BINARY_PATH="${TMPDIR}/atmos"
fi
if [ "${UNAME_OS}" = "windows" ]; then
case "${BINARY_NAME}" in *.exe) ;; *) BINARY_NAME="${BINARY_NAME}.exe" ;; esac
case "${BINARY_PATH}" in *.exe) ;; *) BINARY_PATH="${BINARY_PATH}.exe" ;; esac
fi
if [ ! -f "${BINARY_PATH}" ]; then
log_crit "Binary not found: ${BINARY_PATH}"
log_crit "Listing contents of ${TMPDIR} ..."
if command -v find >/dev/null 2>&1; then
cd "${TMPDIR}" && find .
else
cd "${TMPDIR}" && ls -R .
fi
return 1
fi
progress_clear
# Install the binary
INSTALL_PATH="${BINDIR}/${BINARY_NAME}"
if [ "$DRY_RUN" = "1" ]; then
log_info "[DRY RUN] ${BINARY_NAME} dry-run installation succeeded! (Would install to: ${INSTALL_PATH})"
else
log_info "Installing binary to ${INSTALL_PATH}"
test ! -d "${BINDIR}" && install -d "${BINDIR}"
install "${BINARY_PATH}" "${INSTALL_PATH}"
log_info "${BINARY_NAME} installation complete!"
fi
BINARY_NAME='tenv'
if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then
BINARY_PATH="${TMPDIR}/${ASSET_FILENAME}"
else
BINARY_PATH="${TMPDIR}/tenv"
fi
if [ "${UNAME_OS}" = "windows" ]; then
case "${BINARY_NAME}" in *.exe) ;; *) BINARY_NAME="${BINARY_NAME}.exe" ;; esac
case "${BINARY_PATH}" in *.exe) ;; *) BINARY_PATH="${BINARY_PATH}.exe" ;; esac
fi
if [ ! -f "${BINARY_PATH}" ]; then
log_crit "Binary not found: ${BINARY_PATH}"
log_crit "Listing contents of ${TMPDIR} ..."
if command -v find >/dev/null 2>&1; then
cd "${TMPDIR}" && find .
else
cd "${TMPDIR}" && ls -R .
fi
return 1
fi
progress_clear
# Install the binary
INSTALL_PATH="${BINDIR}/${BINARY_NAME}"
if [ "$DRY_RUN" = "1" ]; then
log_info "[DRY RUN] ${BINARY_NAME} dry-run installation succeeded! (Would install to: ${INSTALL_PATH})"
else
log_info "Installing binary to ${INSTALL_PATH}"
test ! -d "${BINDIR}" && install -d "${BINDIR}"
install "${BINARY_PATH}" "${INSTALL_PATH}"
log_info "${BINARY_NAME} installation complete!"
fi
BINARY_NAME='terraform'
if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then
BINARY_PATH="${TMPDIR}/${ASSET_FILENAME}"
else
BINARY_PATH="${TMPDIR}/terraform"
fi
if [ "${UNAME_OS}" = "windows" ]; then
case "${BINARY_NAME}" in *.exe) ;; *) BINARY_NAME="${BINARY_NAME}.exe" ;; esac
case "${BINARY_PATH}" in *.exe) ;; *) BINARY_PATH="${BINARY_PATH}.exe" ;; esac
fi
if [ ! -f "${BINARY_PATH}" ]; then
log_crit "Binary not found: ${BINARY_PATH}"
log_crit "Listing contents of ${TMPDIR} ..."
if command -v find >/dev/null 2>&1; then
cd "${TMPDIR}" && find .
else
cd "${TMPDIR}" && ls -R .
fi
return 1
fi
progress_clear
# Install the binary
INSTALL_PATH="${BINDIR}/${BINARY_NAME}"
if [ "$DRY_RUN" = "1" ]; then
log_info "[DRY RUN] ${BINARY_NAME} dry-run installation succeeded! (Would install to: ${INSTALL_PATH})"
else
log_info "Installing binary to ${INSTALL_PATH}"
test ! -d "${BINDIR}" && install -d "${BINDIR}"
install "${BINARY_PATH}" "${INSTALL_PATH}"
log_info "${BINARY_NAME} installation complete!"
fi
BINARY_NAME='terragrunt'
if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then
BINARY_PATH="${TMPDIR}/${ASSET_FILENAME}"
else
BINARY_PATH="${TMPDIR}/terragrunt"
fi
if [ "${UNAME_OS}" = "windows" ]; then
case "${BINARY_NAME}" in *.exe) ;; *) BINARY_NAME="${BINARY_NAME}.exe" ;; esac
case "${BINARY_PATH}" in *.exe) ;; *) BINARY_PATH="${BINARY_PATH}.exe" ;; esac
fi
if [ ! -f "${BINARY_PATH}" ]; then
log_crit "Binary not found: ${BINARY_PATH}"
log_crit "Listing contents of ${TMPDIR} ..."
if command -v find >/dev/null 2>&1; then
cd "${TMPDIR}" && find .
else
cd "${TMPDIR}" && ls -R .
fi
return 1
fi
progress_clear
# Install the binary
INSTALL_PATH="${BINDIR}/${BINARY_NAME}"
if [ "$DRY_RUN" = "1" ]; then
log_info "[DRY RUN] ${BINARY_NAME} dry-run installation succeeded! (Would install to: ${INSTALL_PATH})"
else
log_info "Installing binary to ${INSTALL_PATH}"
test ! -d "${BINDIR}" && install -d "${BINDIR}"
install "${BINARY_PATH}" "${INSTALL_PATH}"
log_info "${BINARY_NAME} installation complete!"
fi
BINARY_NAME='terramate'
if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then
BINARY_PATH="${TMPDIR}/${ASSET_FILENAME}"
else
BINARY_PATH="${TMPDIR}/terramate"
fi
if [ "${UNAME_OS}" = "windows" ]; then
case "${BINARY_NAME}" in *.exe) ;; *) BINARY_NAME="${BINARY_NAME}.exe" ;; esac
case "${BINARY_PATH}" in *.exe) ;; *) BINARY_PATH="${BINARY_PATH}.exe" ;; esac
fi
if [ ! -f "${BINARY_PATH}" ]; then
log_crit "Binary not found: ${BINARY_PATH}"
log_crit "Listing contents of ${TMPDIR} ..."
if command -v find >/dev/null 2>&1; then
cd "${TMPDIR}" && find .
else
cd "${TMPDIR}" && ls -R .
fi
return 1
fi
progress_clear
# Install the binary
INSTALL_PATH="${BINDIR}/${BINARY_NAME}"
if [ "$DRY_RUN" = "1" ]; then
log_info "[DRY RUN] ${BINARY_NAME} dry-run installation succeeded! (Would install to: ${INSTALL_PATH})"
else
log_info "Installing binary to ${INSTALL_PATH}"
test ! -d "${BINDIR}" && install -d "${BINDIR}"
install "${BINARY_PATH}" "${INSTALL_PATH}"
log_info "${BINARY_NAME} installation complete!"
fi
BINARY_NAME='tf'
if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then
BINARY_PATH="${TMPDIR}/${ASSET_FILENAME}"
else
BINARY_PATH="${TMPDIR}/tf"
fi
if [ "${UNAME_OS}" = "windows" ]; then
case "${BINARY_NAME}" in *.exe) ;; *) BINARY_NAME="${BINARY_NAME}.exe" ;; esac
case "${BINARY_PATH}" in *.exe) ;; *) BINARY_PATH="${BINARY_PATH}.exe" ;; esac
fi
if [ ! -f "${BINARY_PATH}" ]; then
log_crit "Binary not found: ${BINARY_PATH}"
log_crit "Listing contents of ${TMPDIR} ..."
if command -v find >/dev/null 2>&1; then
cd "${TMPDIR}" && find .
else
cd "${TMPDIR}" && ls -R .
fi
return 1
fi
progress_clear
# Install the binary
INSTALL_PATH="${BINDIR}/${BINARY_NAME}"
if [ "$DRY_RUN" = "1" ]; then
log_info "[DRY RUN] ${BINARY_NAME} dry-run installation succeeded! (Would install to: ${INSTALL_PATH})"
else
log_info "Installing binary to ${INSTALL_PATH}"
test ! -d "${BINDIR}" && install -d "${BINDIR}"
install "${BINARY_PATH}" "${INSTALL_PATH}"
log_info "${BINARY_NAME} installation complete!"
fi
BINARY_NAME='tofu'
if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then
BINARY_PATH="${TMPDIR}/${ASSET_FILENAME}"
else
BINARY_PATH="${TMPDIR}/tofu"
fi
if [ "${UNAME_OS}" = "windows" ]; then
case "${BINARY_NAME}" in *.exe) ;; *) BINARY_NAME="${BINARY_NAME}.exe" ;; esac
case "${BINARY_PATH}" in *.exe) ;; *) BINARY_PATH="${BINARY_PATH}.exe" ;; esac
fi
if [ ! -f "${BINARY_PATH}" ]; then
log_crit "Binary not found: ${BINARY_PATH}"
log_crit "Listing contents of ${TMPDIR} ..."
if command -v find >/dev/null 2>&1; then
cd "${TMPDIR}" && find .
else
cd "${TMPDIR}" && ls -R .
fi
return 1
fi
progress_clear
# Install the binary
INSTALL_PATH="${BINDIR}/${BINARY_NAME}"
if [ "$DRY_RUN" = "1" ]; then
log_info "[DRY RUN] ${BINARY_NAME} dry-run installation succeeded! (Would install to: ${INSTALL_PATH})"
else
log_info "Installing binary to ${INSTALL_PATH}"
test ! -d "${BINDIR}" && install -d "${BINDIR}"
install "${BINARY_PATH}" "${INSTALL_PATH}"
log_info "${BINARY_NAME} installation complete!"
fi
}
# --- Configuration ---
NAME='tenv'
REPO='tofuutils/tenv'
EXT='.tar.gz'
# use in logging routines
log_prefix() {
echo "${REPO}"
}
parse_args "$@"
progress_init
progress_pulse_start
# --- Determine target platform ---
OS="${BINSTALLER_OS:-$(uname_os)}"
UNAME_OS="${OS}"
ARCH="${BINSTALLER_ARCH:-$(uname_arch)}"
UNAME_ARCH="${ARCH}"
log_info "Detected Platform: ${OS}/${ARCH}"
# --- Validate platform ---
uname_os_check "$OS"
uname_arch_check "$ARCH"
tag_to_version
resolve_asset_filename
execute