Installer source
Read the script before it touches your server.
This page renders the same /install.sh file the site serves for the TailHome install command, so you can inspect it without Chrome treating the shell script as a download.
public/install.sh
#!/usr/bin/env bash
set -Eeuo pipefail
TAILHOME_VERSION="0.1.0"
TAILHOME_ORIGIN="${TAILHOME_ORIGIN:-https://tailhome.blackielabs.com}"
INSTALL_MODE="${TAILHOME_INSTALL_MODE:-full}"
fail() {
printf 'error: %s\n' "$*" >&2
exit 1
}
usage() {
cat <<USAGE
TailHome ${TAILHOME_VERSION}
Usage:
./install.sh [--cli-only] [installer options]
Modes:
full Linux only. Installs Tailscale, Docker, the TailHome stack, and the CLI.
cli-only Installs only the tailhome CLI binary.
Environment:
TAILHOME_INSTALL_MODE=full|cli-only
TAILHOME_INSTALL_URL=https://example.com/tailhome-linux-amd64.tar.gz
TAILHOME_DOWNLOAD_ATTEMPTS=5
TAILHOME_BIN_DIR=/usr/local/bin
TAILHOME_ORIGIN=https://tailhome.blackielabs.com
USAGE
}
download() {
local url="$1"
local output="$2"
local attempts="${TAILHOME_DOWNLOAD_ATTEMPTS:-5}"
local retry_delay="${TAILHOME_DOWNLOAD_RETRY_DELAY:-2}"
local attempt=1
local status=1
local received=0
local -a resume_args
[[ "${attempts}" =~ ^[1-9][0-9]*$ ]] || fail "TAILHOME_DOWNLOAD_ATTEMPTS must be a positive integer"
[[ "${retry_delay}" =~ ^[0-9]+$ ]] || fail "TAILHOME_DOWNLOAD_RETRY_DELAY must be a non-negative integer"
while [[ "${attempt}" -le "${attempts}" ]]; do
resume_args=()
if [[ -s "${output}" ]]; then
received="$(wc -c < "${output}" | tr -d ' ')"
resume_args=(--continue-at -)
printf 'Resuming download at byte %s (attempt %s/%s)\n' "${received}" "${attempt}" "${attempts}" >&2
fi
if command -v curl >/dev/null 2>&1; then
if curl -fsSL --connect-timeout 15 "${resume_args[@]}" "${url}" -o "${output}"; then
return 0
else
status=$?
fi
# Exit 33 means the server rejected resume. Start the next attempt cleanly.
if [[ "${status}" -eq 33 ]]; then
rm -f -- "${output}"
fi
elif command -v wget >/dev/null 2>&1; then
if wget -q --tries=1 --timeout=30 --continue -O "${output}" "${url}"; then
return 0
else
status=$?
fi
else
fail "curl or wget is required"
fi
if [[ "${attempt}" -ge "${attempts}" ]]; then
printf 'error: download failed after %s attempts: %s\n' "${attempts}" "${url}" >&2
return "${status}"
fi
printf 'warning: download interrupted (exit %s); retrying in %ss\n' "${status}" "${retry_delay}" >&2
sleep "${retry_delay}"
attempt=$((attempt + 1))
done
}
detect_os() {
case "$(uname -s)" in
Linux) printf 'linux' ;;
Darwin) printf 'darwin' ;;
MINGW*|MSYS*|CYGWIN*) printf 'windows' ;;
*) fail "unsupported OS: $(uname -s)" ;;
esac
}
detect_arch() {
case "$(uname -m)" in
x86_64|amd64) printf 'amd64' ;;
arm64|aarch64) printf 'arm64' ;;
armv7l) printf 'armv7' ;;
armv6l) printf 'armv6' ;;
*) fail "unsupported architecture: $(uname -m)" ;;
esac
}
platform_asset() {
local os_name arch extension
os_name="$(detect_os)"
arch="$(detect_arch)"
extension=""
if [[ "${os_name}" == "windows" ]]; then
extension=".exe"
fi
printf 'tailhome-%s-%s%s' "${os_name}" "${arch}" "${extension}"
}
verify_bundle() {
local archive="$1"
local checksum_file="$2"
local expected actual
if [[ ! -s "${checksum_file}" ]] || ! command -v sha256sum >/dev/null 2>&1; then
return
fi
expected="$(awk 'NR == 1 {print $1}' "${checksum_file}")"
actual="$(sha256sum "${archive}" | awk '{print $1}')"
[[ -n "${expected}" && "${expected}" == "${actual}" ]] || fail "TailHome bundle checksum verification failed"
}
download_bundle() {
local destination="$1"
local archive_url checksum_url checksum_file asset
asset="$(platform_asset)"
archive_url="${TAILHOME_INSTALL_URL:-${TAILHOME_ORIGIN}/downloads/${asset}.tar.gz}"
checksum_url="${TAILHOME_INSTALL_CHECKSUM_URL:-${archive_url}.sha256}"
checksum_file="${destination}.sha256"
printf 'Downloading TailHome from %s\n' "${archive_url}"
download "${archive_url}" "${destination}"
if download "${checksum_url}" "${checksum_file}" 2>/dev/null; then
verify_bundle "${destination}" "${checksum_file}"
else
printf 'warning: checksum file was unavailable; continuing with the HTTPS download\n' >&2
fi
}
extract_bundle() {
local archive="$1"
local destination="$2"
local entry
while IFS= read -r entry; do
case "${entry}" in
/*|../*|*/../*|*/..)
fail "unsafe path in TailHome bundle: ${entry}"
;;
esac
done < <(tar -tzf "${archive}")
tar -xzf "${archive}" -C "${destination}"
}
install_cli_from_bundle() {
local bundle_root="$1"
local os_name arch asset source_bin bin_dir
local -a sudo_cmd
os_name="$(detect_os)"
arch="$(detect_arch)"
if [[ "${os_name}" == "windows" ]]; then
bin_dir="${TAILHOME_BIN_DIR:-${HOME}/bin}"
else
bin_dir="${TAILHOME_BIN_DIR:-/usr/local/bin}"
fi
asset="$(platform_asset)"
source_bin="${bundle_root}/dist/${asset}"
[[ -x "${source_bin}" || -f "${source_bin}" ]] || fail "the bundle does not contain a CLI for ${os_name}/${arch}"
if [[ "${os_name}" == "windows" ]]; then
mkdir -p "${bin_dir}"
cp "${source_bin}" "${bin_dir}/tailhome.exe"
chmod +x "${bin_dir}/tailhome.exe" 2>/dev/null || true
printf 'TailHome CLI installed at %s\n' "${bin_dir}/tailhome.exe"
printf 'Add %s to PATH if it is not already available.\n' "${bin_dir}"
return
fi
if [[ "${TAILHOME_USE_SUDO:-1}" == "0" || "${EUID}" -eq 0 ]]; then
sudo_cmd=()
else
command -v sudo >/dev/null 2>&1 || fail "sudo is required to install to ${bin_dir}; set TAILHOME_BIN_DIR to a writable directory"
sudo_cmd=(sudo)
fi
"${sudo_cmd[@]}" mkdir -p "${bin_dir}"
"${sudo_cmd[@]}" cp "${source_bin}" "${bin_dir}/tailhome"
"${sudo_cmd[@]}" chmod +x "${bin_dir}/tailhome"
printf 'TailHome CLI installed at %s\n' "${bin_dir}/tailhome"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--cli-only)
INSTALL_MODE="cli-only"
shift
;;
-h|--help)
usage
exit 0
;;
*)
break
;;
esac
done
command -v tar >/dev/null 2>&1 || fail "tar is required"
# BASH_SOURCE is empty when this script is streamed with `curl | bash`.
# Only resolve a checkout-relative installer when a real source file exists.
SCRIPT_SOURCE="${BASH_SOURCE[0]-}"
LOCAL_INSTALLER=""
if [[ -n "${SCRIPT_SOURCE}" && -f "${SCRIPT_SOURCE}" ]]; then
ROOT_DIR="$(cd "$(dirname "${SCRIPT_SOURCE}")" && pwd)"
if [[ -x "${ROOT_DIR}/apps/tailhome/install.sh" ]]; then
LOCAL_INSTALLER="${ROOT_DIR}/apps/tailhome/install.sh"
fi
fi
if [[ "${INSTALL_MODE}" == "full" && "$(detect_os)" == "linux" && -n "${LOCAL_INSTALLER}" ]]; then
exec "${LOCAL_INSTALLER}" "$@"
fi
TMP_DIR="$(mktemp -d)"
ARCHIVE="${TMP_DIR}/tailhome.tar.gz"
cleanup() {
rm -rf -- "${TMP_DIR}"
}
trap cleanup EXIT
download_bundle "${ARCHIVE}"
extract_bundle "${ARCHIVE}" "${TMP_DIR}"
BUNDLE_ROOT="${TMP_DIR}/tailhome"
[[ -d "${BUNDLE_ROOT}" ]] || fail "the downloaded bundle is missing its tailhome directory"
if [[ "${INSTALL_MODE}" == "cli-only" || "$(detect_os)" != "linux" ]]; then
install_cli_from_bundle "${BUNDLE_ROOT}"
exit 0
fi
REMOTE_INSTALLER="${BUNDLE_ROOT}/install.sh"
[[ -f "${REMOTE_INSTALLER}" ]] || fail "install.sh was not found in the downloaded bundle"
chmod +x "${REMOTE_INSTALLER}"
"${REMOTE_INSTALLER}" "$@"