Skip to content

Instantly share code, notes, and snippets.

@mndrake
Created February 12, 2026 15:17
Show Gist options
  • Select an option

  • Save mndrake/fd1f35fa669d06a56bc31e94180fb428 to your computer and use it in GitHub Desktop.

Select an option

Save mndrake/fd1f35fa669d06a56bc31e94180fb428 to your computer and use it in GitHub Desktop.
#!/bin/sh
# Exit immediately if a command exits with a non-zero status.
# Treat unset variables as an error when substituting.
set -eu
# Function to be called on script exit for cleanup purposes.
cleanup() {
# Clean up any temporary directories
if [ -n "${td:-}" ]; then
rm -rf "$td"
fi
}
# Register the cleanup function to be called on EXIT.
trap cleanup EXIT
# Color codes
GRAY='\033[0;90m'
RED='\033[0;31m'
NC='\033[0m' # No Color
TARGET_VERSION=""
log() {
printf "install.sh: %s\n" "$1"
}
log_grey() {
printf "${GRAY}install.sh: %s${NC}\n" "$1"
}
# debug messages only show where there's an error in the installation
# when the install/update is triggered from dbt system update
log_debug() {
printf "${GRAY}install.sh: %s${NC}\n" "$1" 2>&1
}
err_and_exit() {
Message="${1:-}"
AdditionalInfo="${2:-}"
info_suffix=""
if [ -n "$AdditionalInfo" ]; then
info_suffix=" - $AdditionalInfo"
fi
printf "${RED}ERROR: %s%s ${NC}\n" "$Message" "$info_suffix"
exit 1
}
need() {
if ! command -v "$1" >/dev/null 2>&1; then
err_and_exit "need $1 (command not found)"
fi
}
# Function to install jq automatically
install_jq() {
jq_version="1.7.1"
jq_url=""
jq_binary_name="jq"
temp_dir=""
log_grey "jq not found, installing automatically..."
# Detect platform for jq download
os=$(uname -s | tr '[:upper:]' '[:lower:]')
arch=$(uname -m)
case "$os" in
linux)
case "$arch" in
x86_64)
jq_url="https://github.com/jqlang/jq/releases/download/jq-${jq_version}/jq-linux-amd64"
;;
aarch64|arm64)
jq_url="https://github.com/jqlang/jq/releases/download/jq-${jq_version}/jq-linux-arm64"
;;
*)
err_and_exit "Unsupported architecture for automatic jq installation: $arch" "Please install jq manually and re-run this script"
;;
esac
;;
darwin)
case "$arch" in
x86_64)
jq_url="https://github.com/jqlang/jq/releases/download/jq-${jq_version}/jq-macos-amd64"
;;
arm64)
jq_url="https://github.com/jqlang/jq/releases/download/jq-${jq_version}/jq-macos-arm64"
;;
*)
err_and_exit "Unsupported architecture for automatic jq installation: $arch" "Please install jq manually and re-run this script"
;;
esac
;;
*)
err_and_exit "Unsupported OS for automatic jq installation: $os" "Please install jq manually and re-run this script"
;;
esac
# Create temporary directory for jq installation
temp_dir=$(mktemp -d || mktemp -d -t tmp)
# Download jq
log_debug "Downloading jq from: $jq_url"
if ! curl -sL -f -o "$temp_dir/jq" "$jq_url"; then
rm -rf "$temp_dir"
err_and_exit "Failed to download jq. Please install jq manually and re-run this script"
fi
# Make jq executable
chmod +x "$temp_dir/jq"
# Determine where to install jq
jq_install_dir=""
if [ -w "/usr/local/bin" ] 2>/dev/null; then
jq_install_dir="/usr/local/bin"
elif [ -d "$HOME/.local/bin" ]; then
jq_install_dir="$HOME/.local/bin"
# Ensure it's in PATH for this session
PATH=$PATH:$HOME/.local/bin
export PATH
else
mkdir -p "$HOME/.local/bin"
jq_install_dir="$HOME/.local/bin"
PATH=$PATH:$HOME/.local/bin
export PATH
fi
# Install jq
if ! cp "$temp_dir/jq" "$jq_install_dir/jq"; then
rm -rf "$temp_dir"
err_and_exit "Failed to install jq to $jq_install_dir. Please install jq manually and re-run this script"
fi
rm -rf "$temp_dir"
log_debug "Successfully installed jq to $jq_install_dir/jq"
# Verify installation
if ! command -v jq >/dev/null 2>&1; then
err_and_exit "jq installed but not found in PATH. You may need to restart your terminal or update your PATH"
fi
return 0
}
help() {
echo "Usage: install.sh [options]"
echo ""
echo "Options:"
echo " --update, -u Update to latest or specified version"
echo " --version VER Install version VER"
echo " --target TARGET Install for target platform TARGET"
echo " --to DEST Install to DEST"
echo " --help, -h Show this help text"
}
update=false
while [ $# -gt 0 ]; do
case $1 in
--update | -u)
update=true
;;
--help | -h)
help
exit 0
;;
--version)
version=$2
shift
;;
--package | -p)
package=$2
shift
;;
--target)
target=$2
shift
;;
--to)
dest=$2
shift
;;
*) ;;
esac
shift
done
# Check for jq and install if missing
check_jq() {
if ! command -v jq >/dev/null 2>&1; then
if ! install_jq; then
err_and_exit "jq is required but could not be installed automatically. Please install jq manually and re-run this script."
fi
fi
}
check_dependencies() {
version_arg="${1:-}"
target_arg="${2:-}"
# Dependencies
need basename
need curl
need install
need mkdir
need mktemp
need tar
# optional dependencies
if [ -z "$version_arg" ] || [ -z "$target_arg" ]; then
need cut
fi
if [ -z "$version_arg" ]; then
need rev
fi
if [ -z "$target_arg" ]; then
need grep
fi
if [ -z "${dest:-}" ]; then
dest="$HOME/.local/bin"
else
# Convert relative path to absolute
case "$dest" in
/*) ;; # Already absolute path
*) dest="$PWD/$dest" ;; # Convert relative to absolute
esac
fi
}
# Check version of an installed binary
# Usage: check_binary_version binary_path binary_name
# Returns: version string if found, empty string if not found
check_binary_version() {
binary_path="$1"
binary_name="$2"
version=""
if [ -f "$binary_path" ] && [ -x "$binary_path" ]; then
version=$("$binary_path" --version 2>/dev/null | cut -d ' ' -f 2 || echo "")
fi
echo "$version"
}
# Compare installed version with target version
# Usage: compare_versions current_version target_version is_latest
# Returns: 0 if versions match, 1 if they don't match
compare_versions() {
current_version="$1"
target_version="$2"
version="$3"
package="$4"
if [ "$current_version" = "$target_version" ]; then
if [ -z "$version" ]; then
log "Latest $package version $target_version is already installed"
else
log "$package version $target_version is already installed"
fi
return 0
fi
return 1
}
# validate if a version exists on the CDN
# Usage: version_exists version target
# Returns: 0 if version exists, error if it doesn't
check_version_exists() {
version="$1"
target="$2"
url="https://public.cdn.getdbt.com/fs/cli/fs-v$version-$target.tar.gz"
log_debug "Checking if version $version exists on CDN: $url"
if ! curl -sLI -f "$url" >/dev/null 2>&1; then
err_and_exit "Version $version not found on CDN. Please check available versions and try again."
fi
return 0
}
# Detect and set target platform
# Usage: detect_target
# Returns: target platform string
detect_target_platform() {
cpu_arch_target=$(uname -m)
operating_system=$(uname -s | tr '[:upper:]' '[:lower:]')
target=""
if [ "$operating_system" = "linux" ]; then
if [ "$cpu_arch_target" = "arm64" ] || [ "$cpu_arch_target" = "aarch64" ]; then
target="aarch64-unknown-linux-gnu"
elif [ "$cpu_arch_target" = "x86_64" ]; then
target="x86_64-unknown-linux-gnu"
else
err_and_exit "Unsupported CPU Architecture: $cpu_arch_target"
fi
elif [ "$operating_system" = "darwin" ]; then
if [ "$cpu_arch_target" = "arm64" ]; then
target="aarch64-apple-darwin"
else
target="x86_64-apple-darwin"
fi
else
err_and_exit "Unsupported OS: $operating_system"
fi
echo "$target"
}
show_path_instructions() {
log_grey ""
log_grey "NOTE: $dest may not be in your PATH."
log_grey "To add it permanently, run one of these commands depending on your shell:"
log_grey " For bash/zsh: echo 'export PATH=\"\$PATH:$dest\"' >> ~/.bashrc # or ~/.zshrc"
log_grey ""
log_grey "To use dbt in this session immediately, run:"
log_grey " export PATH=\"\$PATH:$dest\""
log_grey ""
log_grey "Then restart your terminal or run 'source ~/.bashrc' (or equivalent) for permanent changes"
}
# Setup shell config file and PATH
# Usage: setup_shell_config dest_path
setup_shell_config() {
dest="$1"
config_file=""
shell_name=""
# Detect shell and config file early
if [ -n "$SHELL" ]; then
shell_name=$(basename "$SHELL")
else
if [ -f "$HOME/.bashrc" ]; then
shell_name="bash"
elif [ -f "$HOME/.profile" ]; then
shell_name="sh"
else
shell_name=$(ps -p "$PPID" -o comm= | sed 's/.*\///')
fi
fi
# Set config file based on shell
if [ "$shell_name" = "zsh" ]; then
config_file="$HOME/.zshrc"
elif [ "$shell_name" = "bash" ]; then
config_file="$HOME/.bashrc"
elif [ "$shell_name" = "fish" ]; then
config_file="$HOME/.config/fish/config.fish"
fi
if [ -z "$config_file" ]; then
log_grey "NOTE: Failed to identify config file."
show_path_instructions
return 1
fi
# check if the config file exists or not and create it if it doesn't
if [ ! -f "$config_file" ]; then
if touch "$config_file"; then
log_debug "Created config file $config_file"
else
log_grey "Note: Failed to create config file $config_file."
show_path_instructions
return 1
fi
fi
local needs_config_path_update=false
if ! grep -q "export PATH=\"\$PATH:$dest\"" "$config_file" 2>/dev/null; then
needs_config_path_update=true
fi
local needs_path_update=false
if ! echo "$PATH" | grep -q "$dest"; then
needs_path_update=true
fi
# Check if aliases need to be updated
local needs_alias_update=false
if ! grep -q "alias dbtf=$dest/dbt" "$config_file" 2>/dev/null; then
needs_alias_update=true
fi
if [ "$shell_name" != "fish" ]; then
if [ "$needs_config_path_update" = true ]; then
{
echo "" >> "$config_file" && \
echo "# Added by dbt installer" >> "$config_file" && \
echo "export PATH=\"\$PATH:$dest\"" >> "$config_file" && \
log_debug "Added $dest to PATH in $config_file"
} || {
log_grey "NOTE: Failed to modify $config_file."
show_path_instructions
return 1
}
fi
else
if [ "$needs_config_path_update" = true ]; then
{
echo "fish_add_path $dest" >> "$config_file"
log_debug "Added $dest to PATH in $config_file"
} || {
log_grey "NOTE: Failed to modify $config_file."
show_path_instructions
return 1
}
fi
fi
if [ "$needs_path_update" = true ]; then
log "To use dbt in this session, run:" && \
log " source $config_file" && \
log "" && \
log "The PATH change will be permanent for new terminal sessions."
fi
# Handle alias updates separately
if [ "$needs_alias_update" = true ]; then
{
echo "" >> "$config_file" && \
echo "# dbt aliases" >> "$config_file" && \
echo "alias dbtf=$dest/dbt" >> "$config_file" && \
log "Added alias dbtf to $config_file" && \
log "To run with dbtf in this session, run: source $config_file"
} || {
log_grey "NOTE: Failed to add aliases to $config_file."
return 1
}
fi
return 0
}
# Determine version to install
# Usage: determine_version [specific_version]
# Returns: target version string
determine_version() {
specific_version="$1"
version_url="https://public.cdn.getdbt.com/fs/versions.json"
versions=""
TARGET_VERSION=""
log_debug "Fetching versions from: $version_url"
versions=$(curl -s -f "$version_url") || err_and_exit "Failed to fetch versions from $version_url"
if [ -z "$versions" ]; then
err_and_exit "No version information received from $version_url"
fi
if [ -z "$specific_version" ];then
log_grey "Checking for latest version"
TARGET_VERSION=$(echo "$versions" | jq -r ".latest.tag" | sed 's/^v//') || err_and_exit "Failed to parse latest version from versions data."
log_grey "Latest available version: $TARGET_VERSION"
# check the version that tag maps to, if the tag exists
elif echo "$versions" | jq -e "has(\"$specific_version\")" > /dev/null;then
log_grey "Checking for $specific_version version"
TARGET_VERSION=$(echo "$versions" | jq -r ".\"$specific_version\".tag" | sed 's/^v//') || err_and_exit "Failed to parse requested version $specific_version from versions data."
log_grey "$specific_version available version: $TARGET_VERSION"
else
TARGET_VERSION="$specific_version"
log_grey "Requested version: $TARGET_VERSION"
fi
}
# Display ASCII art for a package
# Usage: display_ascii_art package_name version
display_ascii_art() {
package_name="$1"
version="$2"
if [ "$package_name" = "dbt" ]; then
cat<<EOF
===== ===== ┓┓
========= ========= ┏┫┣┓╋
=========== >======== ┗┻┗┛┗
====================== ███████╗██╗ ██╗███████╗██╗ ██████╗ ███╗ ██╗
==================== ██╔════╝██║ ██║██╔════╝██║██╔═══██╗████╗ ██║
========--======== █████╗ ██║ ██║███████╗██║██║ ██║██╔██╗ ██║
=====- -===== ██╔══╝ ██║ ██║╚════██║██║██║ ██║██║╚██╗██║
========--======== ██╔══╝ ██║ ██║╚════██║██║██║ ██║██║╚██╗██║
==================== ██║ ╚██████╔╝███████║██║╚██████╔╝██║ ╚████║
====================== ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝
========< ============ ┌─┐┌┐┌┌─┐┬┌┐┌┌─┐
========= ========== ├┤ ││││ ┬││││├┤
===== ===== └─┘┘└┘└─┘┴┘└┘└─┘ $version
EOF
else
log "Successfully installed $package_name $version to: $dest"
fi
}
# Install a package
# Usage: install_package package_name version target dest update
# Returns: 0 on success, 1 on failure
install_package() {
package_name="$1"
version="$2"
target="$3"
dest="$4"
update="$5"
td=""
url=""
current_version=""
# Check if already installed and get version
current_version=$(check_binary_version "$dest/$package_name" "$package_name")
if [ -n "$current_version" ] && [ "$current_version" = "$version" ]; then
log "$package_name version $version is already installed"
return 0
elif [ -n "$current_version" ]; then
log_debug "Current installed $package_name version: $current_version"
fi
# If we get here, version is different, so check if we can proceed
if [ -e "$dest/$package_name" ] && [ "$update" = false ]; then
err_and_exit "$package_name already exists in $dest, use the --update flag to reinstall"
fi
log "Installing $package_name to: $dest"
# Create the directory if it doesn't exist
mkdir -p "$dest"
# Create temp directory
td=$(mktemp -d || mktemp -d -t tmp)
# Construct URL based on package
case "$package_name" in
"dbt")
url="https://public.cdn.getdbt.com/fs/cli/fs-v$version-$target.tar.gz"
;;
"dbt-lsp")
url="https://public.cdn.getdbt.com/fs/lsp/fs-lsp-v$version-$target.tar.gz"
;;
*)
err_and_exit "Invalid package name: $package_name"
;;
esac
log_debug "Downloading: $url"
# Download and extract
if ! curl -sL "$url" | tar -C "$td" -xz; then
err_and_exit "Failed to extract package. The downloaded archive appears to be invalid."
fi
# Check if any files were extracted
if [ ! -d "$td" ] || [ -z "$(ls -A "$td")" ]; then
err_and_exit "No files were extracted from the archive"
fi
for f in "$(cd "$td" && find . -type f)"; do
[ -x "$td/$f" ] || {
log_debug "File $f is not executable, skipping"
continue
}
if [ -e "$dest/$package_name" ] && [ "$update" = true ]; then
# Remove file - no sudo needed for home directory
rm -f "$dest/$package_name" || {
err_and_exit "Error: Failed to remove existing $package_name binary."
}
fi
log_debug "Moving $f to $dest/$package_name"
# Ensure the destination directory exists
mkdir -p "$dest" || err_and_exit "Error: Failed to create installation directory: $dest"
# Install the binary and pipe its verbose output to log_debug
install -v -m 755 "$td/$f" "$dest/$package_name" 2>&1 | while IFS= read -r line; do
log_debug "$line"
done || {
err_and_exit "Error: Failed to install $package_name binary."
}
done
display_ascii_art "$package_name" "$version"
if [ "$update" = true ] && [ -n "$current_version" ]; then
log_grey "Successfully updated $package_name from $current_version to $version"
fi
return 0
}
# Install packages based on selection
# Usage: install_packages package target_version target dest update
# Returns: 0 on success, 1 on failure
install_packages() {
package="$1"
target_version="$2"
target="$3"
dest="$4"
update="$5"
current_dbt_version=""
current_lsp_version=""
dbt_needs_update=false
lsp_needs_update=false
# Check if versions match
if [ "$package" = "all" ] || [ "$package" = "dbt" ]; then
current_dbt_version=$(check_binary_version "$dest/dbt" "dbt")
if ! compare_versions "$current_dbt_version" "$target_version" "$version" "dbt"; then
dbt_needs_update=true
fi
fi
if [ "$package" = "all" ] || [ "$package" = "dbt-lsp" ]; then
current_lsp_version=$(check_binary_version "$dest/dbt-lsp" "dbt-lsp")
if ! compare_versions "$current_lsp_version" "$target_version" "$version" "dbt-lsp"; then
lsp_needs_update=true
fi
fi
# Exit if no updates needed
if [ "$dbt_needs_update" = false ] && [ "$lsp_needs_update" = false ]; then
return 0
fi
# Install packages
if ([ "$package" = "all" ] || [ "$package" = "dbt" ]) && [ "$dbt_needs_update" = true ]; then
if ! install_package "dbt" "$target_version" "$target" "$dest" "$update"; then
return 1
fi
fi
if ([ "$package" = "all" ] || [ "$package" = "dbt-lsp" ]) && [ "$lsp_needs_update" = true ]; then
if ! install_package "dbt-lsp" "$target_version" "$target" "$dest" "$update"; then
return 1
fi
fi
# Setup shell config only for dbt
if [ "$package" = "all" ] || [ "$package" = "dbt" ]; then
setup_shell_config "$dest"
fi
return 0
}
validate_versions() {
dest="$1"
dbt_path="$dest/dbt"
lsp_path="$dest/dbt-lsp"
if [ -f "$dbt_path" ] && [ -x "$dbt_path" ]; then
dbt_version=$("$dbt_path" --version 2>/dev/null | cut -d ' ' -f 2 || echo "")
fi
if [ -f "$lsp_path" ] && [ -x "$lsp_path" ]; then
lsp_version=$("$lsp_path" --version 2>/dev/null | cut -d ' ' -f 2 || echo "")
fi
if [ -z "$dbt_version" ] || [ -z "$lsp_version" ]; then
# if both aren't installed, nothing to compare
return 0
fi
if [ "$dbt_version" != "$lsp_version" ]; then
log_grey "WARNING: dbt and dbt-lsp versions do not match"
fi
return 0
}
main() {
# Initialize variables with defaults if not set by command-line parsing
# These variables are parsed from command-line arguments (lines 155-189)
# If not provided on command line, they will default to empty or a specific value here.
package="${package:-dbt}"
version="${version:-}"
target="${target:-}"
dest="${dest:-$HOME/.local/bin}"
update="${update:-false}"
check_jq
# Determine version to install
determine_version "$version"
target_version="$TARGET_VERSION"
check_dependencies "$version" "$target_version"
target_platform="${target:-$(detect_target_platform)}"
log_grey "Target: $target_platform"
check_version_exists "$target_version" "$target_platform"
install_packages "$package" "$target_version" "$target_platform" "$dest" "$update"
# Only validate versions if not skipped and installing multiple packages
if [ -z "${_FS_SKIP_VERSION_CHECK:-}" ] && [ "$package" = "all" ]; then
validate_versions "$dest"
fi
}
main
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment