install.sh 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. REPO_OWNER="christianlempa"
  4. REPO_NAME="boilerplates"
  5. VERSION="${VERSION:-latest}"
  6. AUTO_INSTALL="${AUTO_INSTALL:-true}"
  7. usage() {
  8. cat <<USAGE
  9. Usage: install.sh [OPTIONS]
  10. Install the boilerplates CLI from GitHub releases via pipx.
  11. Options:
  12. --version VER Version to install (default: "latest")
  13. --no-auto-install Skip automatic dependency installation
  14. -h, --help Show this message
  15. Examples:
  16. curl -fsSL https://raw.githubusercontent.com/$REPO_OWNER/$REPO_NAME/main/scripts/install.sh | bash
  17. curl -fsSL https://raw.githubusercontent.com/$REPO_OWNER/$REPO_NAME/main/scripts/install.sh | bash -s -- --version v1.0.0
  18. Uninstall:
  19. pipx uninstall boilerplates
  20. USAGE
  21. }
  22. log() { printf '[boilerplates] %s\n' "$*" >&2; }
  23. error() { printf '[boilerplates][error] %s\n' "$*" >&2; exit 1; }
  24. detect_os() {
  25. if [[ "$OSTYPE" == "darwin"* ]]; then
  26. OS_TYPE="macos"
  27. elif [[ -f /etc/os-release ]]; then
  28. OS_TYPE="linux"
  29. . /etc/os-release
  30. DISTRO_ID="$ID"
  31. DISTRO_VERSION="${VERSION_ID:-}"
  32. else
  33. OS_TYPE="unknown"
  34. fi
  35. }
  36. install_dependencies_macos() {
  37. log "Detected macOS"
  38. if ! command -v brew >/dev/null 2>&1; then
  39. log "Homebrew not found. Installing Homebrew..."
  40. /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || error "Failed to install Homebrew"
  41. fi
  42. if ! command -v python3 >/dev/null 2>&1; then
  43. log "Installing Python3..."
  44. brew install python3 || error "Failed to install Python3"
  45. fi
  46. if ! command -v git >/dev/null 2>&1; then
  47. log "Installing git..."
  48. brew install git || error "Failed to install git"
  49. fi
  50. if ! command -v pipx >/dev/null 2>&1; then
  51. log "Installing pipx..."
  52. brew install pipx || error "Failed to install pipx"
  53. pipx ensurepath
  54. fi
  55. }
  56. install_dependencies_linux() {
  57. log "Detected Linux ($DISTRO_ID)"
  58. case "$DISTRO_ID" in
  59. ubuntu|debian|pop|linuxmint|elementary)
  60. PKG_MANAGER="apt"
  61. PYTHON_PKG="python3 python3-pip python3-venv"
  62. PIPX_PKG="pipx"
  63. GIT_PKG="git"
  64. UPDATE_CMD="sudo apt update"
  65. INSTALL_CMD="sudo apt install -y"
  66. ;;
  67. fedora|rhel|centos|rocky|almalinux)
  68. PKG_MANAGER="dnf"
  69. PYTHON_PKG="python3 python3-pip"
  70. PIPX_PKG="pipx"
  71. GIT_PKG="git"
  72. UPDATE_CMD="sudo dnf check-update || true"
  73. INSTALL_CMD="sudo dnf install -y"
  74. ;;
  75. opensuse*|sles)
  76. PKG_MANAGER="zypper"
  77. PYTHON_PKG="python3 python3-pip"
  78. PIPX_PKG="python3-pipx"
  79. GIT_PKG="git"
  80. UPDATE_CMD="sudo zypper refresh"
  81. INSTALL_CMD="sudo zypper install -y"
  82. ;;
  83. arch|archarm|manjaro|endeavouros)
  84. PKG_MANAGER="pacman"
  85. PYTHON_PKG="python python-pip"
  86. PIPX_PKG="python-pipx"
  87. GIT_PKG="git"
  88. UPDATE_CMD="sudo pacman -Sy"
  89. INSTALL_CMD="sudo pacman -S --noconfirm"
  90. ;;
  91. alpine)
  92. PKG_MANAGER="apk"
  93. PYTHON_PKG="python3 py3-pip"
  94. PIPX_PKG="pipx"
  95. GIT_PKG="git"
  96. UPDATE_CMD="sudo apk update"
  97. INSTALL_CMD="sudo apk add"
  98. ;;
  99. *)
  100. log "Unsupported Linux distribution: $DISTRO_ID"
  101. log "Please install manually: python3, pip, git, and pipx"
  102. return 1
  103. ;;
  104. esac
  105. if ! command -v python3 >/dev/null 2>&1; then
  106. log "Installing Python3..."
  107. $UPDATE_CMD
  108. $INSTALL_CMD $PYTHON_PKG || error "Failed to install Python3"
  109. fi
  110. if ! command -v git >/dev/null 2>&1; then
  111. log "Installing git..."
  112. $INSTALL_CMD $GIT_PKG || error "Failed to install git"
  113. fi
  114. if ! python3 -m pip --version >/dev/null 2>&1; then
  115. log "pip not available, installing..."
  116. $INSTALL_CMD $PYTHON_PKG || error "Failed to install pip"
  117. fi
  118. if ! command -v pipx >/dev/null 2>&1 && [[ ! -x "$(python3 -m site --user-base 2>/dev/null)/bin/pipx" ]]; then
  119. log "Installing pipx..."
  120. # Try system package first if available
  121. if [[ -n "${PIPX_PKG:-}" ]]; then
  122. if $INSTALL_CMD $PIPX_PKG >/dev/null 2>&1; then
  123. log "pipx installed from system package"
  124. else
  125. # System package failed, try pip with --break-system-packages
  126. if python3 -m pip install --user --break-system-packages pipx 2>&1 | grep -q "Successfully installed"; then
  127. log "pipx installed via pip"
  128. elif python3 -m pip install --user pipx 2>&1 | grep -q "Successfully installed"; then
  129. log "pipx installed via pip"
  130. else
  131. error "Failed to install pipx. Try installing manually: sudo apt install pipx"
  132. fi
  133. fi
  134. else
  135. # No system package, use pip
  136. if python3 -m pip install --user --break-system-packages pipx 2>&1 | grep -q "Successfully installed"; then
  137. log "pipx installed via pip"
  138. elif python3 -m pip install --user pipx 2>&1 | grep -q "Successfully installed"; then
  139. log "pipx installed via pip"
  140. else
  141. error "Failed to install pipx"
  142. fi
  143. fi
  144. # Ensure pipx is in PATH
  145. if command -v pipx >/dev/null 2>&1; then
  146. pipx ensurepath >/dev/null 2>&1
  147. elif [[ -x "$(python3 -m site --user-base 2>/dev/null)/bin/pipx" ]]; then
  148. "$(python3 -m site --user-base)/bin/pipx" ensurepath >/dev/null 2>&1
  149. fi
  150. fi
  151. }
  152. check_python_version() {
  153. if ! command -v python3 >/dev/null 2>&1; then
  154. # Python not installed yet - will be handled by check_dependencies
  155. return 0
  156. fi
  157. local python_version=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null)
  158. if [[ -z "$python_version" ]]; then
  159. log "Warning: Could not determine Python version"
  160. return 0
  161. fi
  162. local major=$(echo "$python_version" | cut -d. -f1)
  163. local minor=$(echo "$python_version" | cut -d. -f2)
  164. if [[ "$major" -lt 3 ]] || { [[ "$major" -eq 3 ]] && [[ "$minor" -lt 10 ]]; }; then
  165. error "Python 3.10 or higher is required. Found: Python $python_version
  166. Boilerplates requires Python 3.10+ for modern type hint syntax.
  167. Your system has Python $python_version installed.
  168. On AlmaLinux/RHEL/CentOS/Rocky Linux 9:
  169. sudo dnf install python3.11
  170. pipx reinstall --python python3.11 boilerplates
  171. On Debian/Ubuntu:
  172. sudo apt install python3.11
  173. pipx reinstall --python python3.11 boilerplates
  174. Alternatively, use pyenv to install Python 3.11+:
  175. https://github.com/pyenv/pyenv#installation"
  176. fi
  177. log "Python version: $python_version (OK)"
  178. }
  179. check_dependencies() {
  180. local missing_deps=()
  181. command -v tar >/dev/null 2>&1 || missing_deps+=("tar")
  182. command -v mktemp >/dev/null 2>&1 || missing_deps+=("mktemp")
  183. if [[ ${#missing_deps[@]} -gt 0 ]]; then
  184. error "Required system tools missing: ${missing_deps[*]}"
  185. fi
  186. local needs_install=false
  187. if ! command -v python3 >/dev/null 2>&1; then
  188. log "Python3 not found"
  189. needs_install=true
  190. fi
  191. if ! command -v git >/dev/null 2>&1; then
  192. log "git not found"
  193. needs_install=true
  194. fi
  195. if ! python3 -m pip --version >/dev/null 2>&1; then
  196. log "pip not found"
  197. needs_install=true
  198. fi
  199. if ! command -v pipx >/dev/null 2>&1 && [[ ! -x "$(python3 -m site --user-base 2>/dev/null)/bin/pipx" ]]; then
  200. log "pipx not found"
  201. needs_install=true
  202. fi
  203. if [[ "$needs_install" == "true" ]]; then
  204. if [[ "$AUTO_INSTALL" == "true" ]]; then
  205. log "Installing missing dependencies..."
  206. detect_os
  207. if [[ "$OS_TYPE" == "macos" ]]; then
  208. install_dependencies_macos
  209. elif [[ "$OS_TYPE" == "linux" ]]; then
  210. install_dependencies_linux
  211. else
  212. error "Unsupported OS. Please install manually: python3, pip, git, and pipx"
  213. fi
  214. else
  215. error "Missing dependencies. Install: python3, pip, git, and pipx (or run without --no-auto-install)"
  216. fi
  217. fi
  218. if command -v pipx >/dev/null 2>&1; then
  219. PIPX_CMD="pipx"
  220. elif [[ -x "$(python3 -m site --user-base 2>/dev/null)/bin/pipx" ]]; then
  221. PIPX_CMD="$(python3 -m site --user-base)/bin/pipx"
  222. else
  223. error "pipx installation failed or not found in PATH. Try: python3 -m pip install --user pipx && python3 -m pipx ensurepath"
  224. fi
  225. log "All dependencies available"
  226. # Check Python version after dependencies are installed
  227. check_python_version
  228. }
  229. parse_args() {
  230. while [[ $# -gt 0 ]]; do
  231. case "$1" in
  232. --version)
  233. [[ $# -lt 2 ]] && error "--version requires an argument"
  234. [[ "$2" =~ ^- ]] && error "--version requires a version string, not an option"
  235. VERSION="$2"
  236. shift 2
  237. ;;
  238. --no-auto-install)
  239. AUTO_INSTALL="false"
  240. shift
  241. ;;
  242. -h|--help) usage; exit 0 ;;
  243. *) error "Unknown option: $1" ;;
  244. esac
  245. done
  246. }
  247. get_latest_release() {
  248. local api_url="https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/releases/latest"
  249. local result
  250. if command -v curl >/dev/null 2>&1; then
  251. result=$(curl -qfsSL --max-time 10 "$api_url" 2>/dev/null | sed -En 's/.*"tag_name": "([^"]+)".*/\1/p')
  252. elif command -v wget >/dev/null 2>&1; then
  253. result=$(wget --timeout=10 -qO- "$api_url" 2>/dev/null | sed -En 's/.*"tag_name": "([^"]+)".*/\1/p')
  254. else
  255. error "Neither curl nor wget found"
  256. fi
  257. [[ -z "$result" ]] && error "Failed to fetch release information from GitHub"
  258. echo "$result"
  259. }
  260. download_and_extract() {
  261. local version="$1"
  262. # Resolve "latest" to actual version
  263. if [[ "$version" == "latest" ]]; then
  264. log "Fetching latest release..."
  265. version=$(get_latest_release)
  266. log "Latest version: $version"
  267. fi
  268. # Ensure 'v' prefix for URL
  269. local version_tag="$version"
  270. [[ "$version_tag" =~ ^v ]] || version_tag="v$version_tag"
  271. # Strip 'v' prefix for package name
  272. local version_number="${version_tag#v}"
  273. # Download from release assets (sdist)
  274. local url="https://github.com/$REPO_OWNER/$REPO_NAME/releases/download/$version_tag/$REPO_NAME-$version_number.tar.gz"
  275. TEMP_DIR=$(mktemp -d)
  276. local archive="$TEMP_DIR/boilerplates.tar.gz"
  277. log "Downloading $version_tag from release assets..."
  278. if command -v curl >/dev/null 2>&1; then
  279. curl -qfsSL --max-time 30 -o "$archive" "$url" || error "Download failed. URL: $url"
  280. elif command -v wget >/dev/null 2>&1; then
  281. wget --timeout=30 -qO "$archive" "$url" || error "Download failed. URL: $url"
  282. fi
  283. log "Extracting package..."
  284. # Extract the tarball
  285. tar -xzf "$archive" -C "$TEMP_DIR" || error "Extraction failed"
  286. # Find the extracted directory (should be boilerplates-X.Y.Z)
  287. local source_dir=$(find "$TEMP_DIR" -maxdepth 1 -type d -name "$REPO_NAME-*" | head -n1)
  288. [[ -z "$source_dir" ]] && error "Failed to locate extracted files"
  289. # Verify essential files exist
  290. [[ ! -f "$source_dir/setup.py" ]] && [[ ! -f "$source_dir/pyproject.toml" ]] && \
  291. error "Invalid package: missing setup.py or pyproject.toml"
  292. # Return the path to the extracted directory
  293. echo "$source_dir"
  294. }
  295. install_cli() {
  296. local package_path="$1"
  297. local version="$2"
  298. log "Installing CLI via pipx..."
  299. "$PIPX_CMD" ensurepath 2>&1 | grep -v "^$" || true
  300. # Install from tarball
  301. if ! "$PIPX_CMD" install --force "$package_path" >/dev/null 2>&1; then
  302. error "pipx installation failed. Try: pipx uninstall boilerplates && pipx install boilerplates"
  303. fi
  304. log "CLI installed successfully"
  305. # Verify installation
  306. if command -v boilerplates >/dev/null 2>&1; then
  307. log "Command 'boilerplates' is now available"
  308. else
  309. log "Warning: 'boilerplates' command not found in PATH. You may need to restart your shell or run: pipx ensurepath"
  310. fi
  311. }
  312. main() {
  313. parse_args "$@"
  314. # Ensure cleanup on exit
  315. trap '[[ -d "${TEMP_DIR:-}" ]] && rm -rf "$TEMP_DIR"' EXIT
  316. log "Checking dependencies..."
  317. check_dependencies
  318. local package_path=$(download_and_extract "$VERSION")
  319. install_cli "$package_path" "$VERSION"
  320. # Get installed version
  321. local installed_version=$(boilerplates --version 2>/dev/null | grep -oE 'v?[0-9]+\.[0-9]+\.[0-9]+' || echo "unknown")
  322. cat <<EOF
  323. \uf05d Installation complete!
  324. Version: $installed_version
  325. Installed via: pipx
  326. Usage:
  327. boilerplates --help
  328. Update:
  329. curl -qfsSL https://raw.githubusercontent.com/$REPO_OWNER/$REPO_NAME/main/scripts/install.sh | bash
  330. Uninstall:
  331. pipx uninstall boilerplates
  332. EOF
  333. }
  334. main "$@"