install.sh 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. REPO_OWNER="christianlempa"
  4. REPO_NAME="boilerplates"
  5. VERSION="${VERSION:-latest}"
  6. TARGET_DIR="${TARGET_DIR:-$HOME/boilerplates}"
  7. usage() {
  8. cat <<USAGE
  9. Usage: install.sh [OPTIONS]
  10. Install the boilerplates CLI from GitHub releases.
  11. Options:
  12. --path DIR Installation directory (default: "$HOME/boilerplates")
  13. --version VER Version to install (default: "latest")
  14. Examples: latest, v1.0.0, v0.0.1
  15. -h, --help Show this message
  16. Examples:
  17. # Install latest version
  18. curl -fsSL https://raw.githubusercontent.com/christianlempa/boilerplates/main/scripts/install.sh | bash
  19. # Install specific version
  20. curl -fsSL https://raw.githubusercontent.com/christianlempa/boilerplates/main/scripts/install.sh | bash -s -- --version v1.0.0
  21. # Install to custom directory
  22. curl -fsSL https://raw.githubusercontent.com/christianlempa/boilerplates/main/scripts/install.sh | bash -s -- --path ~/my-boilerplates
  23. USAGE
  24. }
  25. log() {
  26. printf '[boilerplates] %s\n' "$*"
  27. }
  28. error() {
  29. printf '[boilerplates][error] %s\n' "$*" >&2
  30. exit 1
  31. }
  32. warn() {
  33. printf '[boilerplates][warn] %s\n' "$*" >&2
  34. }
  35. detect_os() {
  36. if [[ "$OSTYPE" == "linux-gnu"* ]]; then
  37. if command -v apt-get >/dev/null 2>&1; then
  38. echo "debian"
  39. elif command -v dnf >/dev/null 2>&1; then
  40. echo "fedora"
  41. elif command -v yum >/dev/null 2>&1; then
  42. echo "rhel"
  43. elif command -v pacman >/dev/null 2>&1; then
  44. echo "arch"
  45. else
  46. echo "linux"
  47. fi
  48. elif [[ "$OSTYPE" == "darwin"* ]]; then
  49. echo "macos"
  50. else
  51. echo "unknown"
  52. fi
  53. }
  54. ensure_python3() {
  55. if command -v python3 >/dev/null 2>&1; then
  56. log "✓ Python3 is already installed"
  57. return 0
  58. fi
  59. log "Python3 not found. Attempting to install..."
  60. local os_type
  61. os_type=$(detect_os)
  62. case "$os_type" in
  63. debian)
  64. log "Detected Debian/Ubuntu. Installing python3..."
  65. sudo apt-get update && sudo apt-get install -y python3 python3-pip python3-venv
  66. ;;
  67. fedora)
  68. log "Detected Fedora. Installing python3..."
  69. sudo dnf install -y python3 python3-pip
  70. ;;
  71. rhel)
  72. log "Detected RHEL/CentOS. Installing python3..."
  73. sudo yum install -y python3 python3-pip
  74. ;;
  75. arch)
  76. log "Detected Arch Linux. Installing python3..."
  77. sudo pacman -S --noconfirm python python-pip
  78. ;;
  79. macos)
  80. if command -v brew >/dev/null 2>&1; then
  81. log "Detected macOS with Homebrew. Installing python3..."
  82. brew install python3
  83. else
  84. error "Python3 not found and Homebrew is not installed. Please install Python3 manually from https://www.python.org/downloads/"
  85. fi
  86. ;;
  87. *)
  88. error "Could not automatically install Python3 on this system. Please install Python3 manually."
  89. ;;
  90. esac
  91. # Verify installation
  92. if ! command -v python3 >/dev/null 2>&1; then
  93. error "Failed to install Python3. Please install it manually."
  94. fi
  95. log "✓ Python3 installed successfully"
  96. }
  97. ensure_pip() {
  98. if python3 -m pip --version >/dev/null 2>&1; then
  99. log "✓ pip is already installed"
  100. return 0
  101. fi
  102. log "pip not found. Attempting to install..."
  103. # Detect OS and install pip using system package manager (preferred for modern Python)
  104. local os_type
  105. os_type=$(detect_os)
  106. case "$os_type" in
  107. debian)
  108. log "Installing pip via apt..."
  109. sudo apt-get update && sudo apt-get install -y python3-pip python3-venv
  110. ;;
  111. fedora)
  112. log "Installing pip via dnf..."
  113. sudo dnf install -y python3-pip
  114. ;;
  115. rhel)
  116. log "Installing pip via yum..."
  117. sudo yum install -y python3-pip
  118. ;;
  119. arch)
  120. log "Installing pip via pacman..."
  121. sudo pacman -S --noconfirm python-pip
  122. ;;
  123. macos)
  124. # On macOS, pip usually comes with Python from Homebrew
  125. log "pip should be included with Python. Verifying..."
  126. python3 -m ensurepip --default-pip 2>/dev/null || true
  127. ;;
  128. *)
  129. # Fallback: try to install pip using get-pip.py
  130. log "Attempting to install pip using get-pip.py..."
  131. if command -v curl >/dev/null 2>&1; then
  132. curl -fsSL https://bootstrap.pypa.io/get-pip.py | python3
  133. elif command -v wget >/dev/null 2>&1; then
  134. wget -qO- https://bootstrap.pypa.io/get-pip.py | python3
  135. else
  136. error "Could not download pip installer. Please install pip manually."
  137. fi
  138. ;;
  139. esac
  140. # Verify installation
  141. if ! python3 -m pip --version >/dev/null 2>&1; then
  142. error "Failed to install pip. Please install it manually using your system package manager."
  143. fi
  144. log "✓ pip installed successfully"
  145. }
  146. require_command() {
  147. command -v "$1" >/dev/null 2>&1 || error "Required command '$1' not found in PATH"
  148. }
  149. parse_args() {
  150. while [[ $# -gt 0 ]]; do
  151. case "$1" in
  152. --path)
  153. [[ $# -lt 2 ]] && error "--path requires a value"
  154. TARGET_DIR="$2"
  155. shift 2
  156. ;;
  157. --version)
  158. [[ $# -lt 2 ]] && error "--version requires a value"
  159. VERSION="$2"
  160. shift 2
  161. ;;
  162. -h|--help)
  163. usage
  164. exit 0
  165. ;;
  166. *)
  167. error "Unknown option: $1"
  168. ;;
  169. esac
  170. done
  171. }
  172. make_absolute_path() {
  173. python3 - <<'PY' "$TARGET_DIR"
  174. import os, sys
  175. print(os.path.abspath(os.path.expanduser(sys.argv[1])))
  176. PY
  177. }
  178. get_latest_release() {
  179. local api_url="https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/releases/latest"
  180. local release_tag
  181. log "Fetching latest release information..."
  182. if command -v curl >/dev/null 2>&1; then
  183. release_tag=$(curl -fsSL "$api_url" | grep '"tag_name":' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/')
  184. elif command -v wget >/dev/null 2>&1; then
  185. release_tag=$(wget -qO- "$api_url" | grep '"tag_name":' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/')
  186. else
  187. error "Neither curl nor wget found. Please install one of them."
  188. fi
  189. if [[ -z "$release_tag" ]]; then
  190. error "Failed to fetch latest release tag"
  191. fi
  192. echo "$release_tag"
  193. }
  194. download_release() {
  195. local version="$1"
  196. local download_url
  197. # If version is "latest", resolve it to the actual version tag
  198. if [[ "$version" == "latest" ]]; then
  199. version=$(get_latest_release)
  200. log "Latest version is $version"
  201. fi
  202. # Ensure version has 'v' prefix for GitHub releases
  203. if [[ ! "$version" =~ ^v ]]; then
  204. version="v$version"
  205. fi
  206. download_url="https://github.com/$REPO_OWNER/$REPO_NAME/archive/refs/tags/$version.tar.gz"
  207. log "Downloading release $version..."
  208. log "URL: $download_url"
  209. local temp_dir
  210. temp_dir=$(mktemp -d)
  211. trap 'rm -rf "$temp_dir"' EXIT
  212. local archive_file="$temp_dir/boilerplates.tar.gz"
  213. if command -v curl >/dev/null 2>&1; then
  214. curl -fsSL -o "$archive_file" "$download_url" || error "Failed to download release"
  215. elif command -v wget >/dev/null 2>&1; then
  216. wget -qO "$archive_file" "$download_url" || error "Failed to download release"
  217. else
  218. error "Neither curl nor wget found. Please install one of them."
  219. fi
  220. log "Extracting release..."
  221. # Remove existing installation if present
  222. if [[ -d "$TARGET_DIR" ]]; then
  223. log "Removing existing installation at $TARGET_DIR"
  224. rm -rf "$TARGET_DIR"
  225. fi
  226. # Create parent directory
  227. mkdir -p "$(dirname "$TARGET_DIR")"
  228. # Extract with strip-components to remove the top-level directory
  229. tar -xzf "$archive_file" -C "$(dirname "$TARGET_DIR")"
  230. # Rename extracted directory to target name
  231. local extracted_dir
  232. extracted_dir=$(dirname "$TARGET_DIR")/"$REPO_NAME-${version#v}"
  233. if [[ ! -d "$extracted_dir" ]]; then
  234. error "Extraction failed: expected directory $extracted_dir not found"
  235. fi
  236. mv "$extracted_dir" "$TARGET_DIR"
  237. log "Release extracted to $TARGET_DIR"
  238. # Store version info
  239. echo "$version" > "$TARGET_DIR/.installed-version"
  240. }
  241. ensure_pipx() {
  242. if command -v pipx >/dev/null 2>&1; then
  243. log "✓ pipx is already installed"
  244. PIPX_CMD="pipx"
  245. return
  246. fi
  247. log "pipx not found. Installing pipx..."
  248. # Detect OS and try to install via package manager first (preferred for PEP 668 systems)
  249. local os_type
  250. os_type=$(detect_os)
  251. case "$os_type" in
  252. debian)
  253. log "Installing pipx via apt..."
  254. if sudo apt-get update && sudo apt-get install -y pipx; then
  255. log "✓ pipx installed via apt"
  256. # Ensure pipx path is set up
  257. pipx ensurepath >/dev/null 2>&1 || true
  258. else
  259. warn "Failed to install pipx via apt, trying pip..."
  260. install_pipx_with_pip
  261. fi
  262. ;;
  263. fedora)
  264. log "Installing pipx via dnf..."
  265. if sudo dnf install -y pipx; then
  266. log "✓ pipx installed via dnf"
  267. else
  268. warn "Failed to install pipx via dnf, trying pip..."
  269. install_pipx_with_pip
  270. fi
  271. ;;
  272. arch)
  273. log "Installing pipx via pacman..."
  274. if sudo pacman -S --noconfirm python-pipx; then
  275. log "✓ pipx installed via pacman"
  276. else
  277. warn "Failed to install pipx via pacman, trying pip..."
  278. install_pipx_with_pip
  279. fi
  280. ;;
  281. macos)
  282. if command -v brew >/dev/null 2>&1; then
  283. log "Installing pipx via Homebrew..."
  284. if brew install pipx; then
  285. log "✓ pipx installed via Homebrew"
  286. # Ensure pipx path is set up
  287. pipx ensurepath >/dev/null 2>&1 || true
  288. else
  289. warn "Failed to install pipx via Homebrew, trying pip..."
  290. install_pipx_with_pip
  291. fi
  292. else
  293. log "Homebrew not found, installing pipx via pip..."
  294. install_pipx_with_pip
  295. fi
  296. ;;
  297. *)
  298. # Fallback to pip installation
  299. install_pipx_with_pip
  300. ;;
  301. esac
  302. # Try to find pipx command
  303. if command -v pipx >/dev/null 2>&1; then
  304. PIPX_CMD="pipx"
  305. log "✓ pipx is ready to use"
  306. return
  307. fi
  308. # Check in user bin directory
  309. local user_bin
  310. user_bin="$(python3 -m site --user-base 2>/dev/null)/bin"
  311. if [[ -x "$user_bin/pipx" ]]; then
  312. PIPX_CMD="$user_bin/pipx"
  313. log "✓ Found pipx at $PIPX_CMD"
  314. return
  315. fi
  316. error "pipx installed but not found in PATH. Please add $(python3 -m site --user-base)/bin to your PATH or restart your shell."
  317. }
  318. install_pipx_with_pip() {
  319. log "Installing pipx using pip..."
  320. # Try with --user flag first (works on most systems)
  321. if python3 -m pip install --user pipx 2>/dev/null; then
  322. log "✓ pipx installed via pip --user"
  323. return
  324. fi
  325. # If that fails due to externally-managed-environment, try with --break-system-packages
  326. # (only as a last resort and with clear warning)
  327. warn "System has PEP 668 restrictions. Attempting installation with --break-system-packages..."
  328. if python3 -m pip install --user --break-system-packages pipx 2>/dev/null; then
  329. log "✓ pipx installed (with --break-system-packages)"
  330. return
  331. fi
  332. error "Failed to install pipx. Please install it manually: sudo apt install pipx (Debian/Ubuntu) or pip install --user pipx"
  333. }
  334. pipx_install() {
  335. "${PIPX_CMD}" ensurepath >/dev/null 2>&1 || warn "pipx ensurepath failed; make sure pipx's bin dir is on PATH"
  336. log "Installing/updating boilerplates via pipx"
  337. "${PIPX_CMD}" install --editable --force "$TARGET_DIR"
  338. }
  339. check_current_version() {
  340. if [[ -f "$TARGET_DIR/.installed-version" ]]; then
  341. cat "$TARGET_DIR/.installed-version"
  342. else
  343. echo "unknown"
  344. fi
  345. }
  346. main() {
  347. parse_args "$@"
  348. log "Checking system dependencies..."
  349. # Ensure required tools are available
  350. require_command tar
  351. # Ensure Python3, pip, and pipx are installed
  352. ensure_python3
  353. ensure_pip
  354. TARGET_DIR="$(make_absolute_path)"
  355. # Check if already installed
  356. local current_version
  357. current_version=$(check_current_version)
  358. if [[ "$current_version" != "unknown" ]]; then
  359. log "Currently installed version: $current_version"
  360. fi
  361. download_release "$VERSION"
  362. ensure_pipx
  363. pipx_install
  364. local pipx_info
  365. pipx_info=$("${PIPX_CMD}" list --short 2>/dev/null | grep -E '^boilerplates' || echo "boilerplates (not detected)")
  366. local installed_version
  367. installed_version=$(check_current_version)
  368. cat <<EOF2
  369. ✓ Installation complete!
  370. Version: $installed_version
  371. Location: $TARGET_DIR
  372. pipx environment: $pipx_info
  373. To use the CLI:
  374. boilerplate --help
  375. boilerplate compose list
  376. To update to the latest version:
  377. curl -fsSL https://raw.githubusercontent.com/$REPO_OWNER/$REPO_NAME/main/scripts/install.sh | bash
  378. To install a specific version:
  379. curl -fsSL https://raw.githubusercontent.com/$REPO_OWNER/$REPO_NAME/main/scripts/install.sh | bash -s -- --version v1.0.0
  380. EOF2
  381. }
  382. main "$@"