install.sh 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  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. if sudo apt-get update && sudo apt-get install -y python3-pip python3-venv; then
  110. log "✓ pip installed via apt"
  111. else
  112. error "Failed to install pip via apt. Please run: sudo apt-get install python3-pip python3-venv"
  113. fi
  114. ;;
  115. fedora)
  116. log "Installing pip via dnf..."
  117. if sudo dnf install -y python3-pip; then
  118. log "✓ pip installed via dnf"
  119. else
  120. error "Failed to install pip via dnf. Please run: sudo dnf install python3-pip"
  121. fi
  122. ;;
  123. rhel)
  124. log "Installing pip via yum..."
  125. if sudo yum install -y python3-pip; then
  126. log "✓ pip installed via yum"
  127. else
  128. error "Failed to install pip via yum. Please run: sudo yum install python3-pip"
  129. fi
  130. ;;
  131. arch)
  132. log "Installing pip via pacman..."
  133. if sudo pacman -S --noconfirm python-pip; then
  134. log "✓ pip installed via pacman"
  135. else
  136. error "Failed to install pip via pacman. Please run: sudo pacman -S python-pip"
  137. fi
  138. ;;
  139. macos)
  140. # On macOS, pip usually comes with Python from Homebrew
  141. log "pip should be included with Python. Verifying..."
  142. python3 -m ensurepip --default-pip 2>/dev/null || true
  143. ;;
  144. *)
  145. error "Could not detect your OS or package manager. Please install pip manually: sudo apt install python3-pip (Debian/Ubuntu) or equivalent for your system."
  146. ;;
  147. esac
  148. # Verify installation
  149. if ! python3 -m pip --version >/dev/null 2>&1; then
  150. error "Failed to install pip. Please install it manually using your system package manager."
  151. fi
  152. log "✓ pip installed successfully"
  153. }
  154. require_command() {
  155. command -v "$1" >/dev/null 2>&1 || error "Required command '$1' not found in PATH"
  156. }
  157. parse_args() {
  158. while [[ $# -gt 0 ]]; do
  159. case "$1" in
  160. --path)
  161. [[ $# -lt 2 ]] && error "--path requires a value"
  162. TARGET_DIR="$2"
  163. shift 2
  164. ;;
  165. --version)
  166. [[ $# -lt 2 ]] && error "--version requires a value"
  167. VERSION="$2"
  168. shift 2
  169. ;;
  170. -h|--help)
  171. usage
  172. exit 0
  173. ;;
  174. *)
  175. error "Unknown option: $1"
  176. ;;
  177. esac
  178. done
  179. }
  180. make_absolute_path() {
  181. python3 - <<'PY' "$TARGET_DIR"
  182. import os, sys
  183. print(os.path.abspath(os.path.expanduser(sys.argv[1])))
  184. PY
  185. }
  186. get_latest_release() {
  187. local api_url="https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/releases/latest"
  188. local release_tag
  189. log "Fetching latest release information..."
  190. if command -v curl >/dev/null 2>&1; then
  191. release_tag=$(curl -fsSL "$api_url" | grep '"tag_name":' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/')
  192. elif command -v wget >/dev/null 2>&1; then
  193. release_tag=$(wget -qO- "$api_url" | grep '"tag_name":' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/')
  194. else
  195. error "Neither curl nor wget found. Please install one of them."
  196. fi
  197. if [[ -z "$release_tag" ]]; then
  198. error "Failed to fetch latest release tag"
  199. fi
  200. echo "$release_tag"
  201. }
  202. download_release() {
  203. local version="$1"
  204. local download_url
  205. # If version is "latest", resolve it to the actual version tag
  206. if [[ "$version" == "latest" ]]; then
  207. version=$(get_latest_release)
  208. log "Latest version is $version"
  209. fi
  210. # Ensure version has 'v' prefix for GitHub releases
  211. if [[ ! "$version" =~ ^v ]]; then
  212. version="v$version"
  213. fi
  214. download_url="https://github.com/$REPO_OWNER/$REPO_NAME/archive/refs/tags/$version.tar.gz"
  215. log "Downloading release $version..."
  216. log "URL: $download_url"
  217. local temp_dir
  218. temp_dir=$(mktemp -d)
  219. trap 'rm -rf "$temp_dir"' EXIT
  220. local archive_file="$temp_dir/boilerplates.tar.gz"
  221. if command -v curl >/dev/null 2>&1; then
  222. curl -fsSL -o "$archive_file" "$download_url" || error "Failed to download release"
  223. elif command -v wget >/dev/null 2>&1; then
  224. wget -qO "$archive_file" "$download_url" || error "Failed to download release"
  225. else
  226. error "Neither curl nor wget found. Please install one of them."
  227. fi
  228. log "Extracting release..."
  229. # Remove existing installation if present
  230. if [[ -d "$TARGET_DIR" ]]; then
  231. log "Removing existing installation at $TARGET_DIR"
  232. rm -rf "$TARGET_DIR"
  233. fi
  234. # Create parent directory
  235. mkdir -p "$(dirname "$TARGET_DIR")"
  236. # Extract with strip-components to remove the top-level directory
  237. tar -xzf "$archive_file" -C "$(dirname "$TARGET_DIR")"
  238. # Rename extracted directory to target name
  239. local extracted_dir
  240. extracted_dir=$(dirname "$TARGET_DIR")/"$REPO_NAME-${version#v}"
  241. if [[ ! -d "$extracted_dir" ]]; then
  242. error "Extraction failed: expected directory $extracted_dir not found"
  243. fi
  244. mv "$extracted_dir" "$TARGET_DIR"
  245. log "Release extracted to $TARGET_DIR"
  246. # Store version info
  247. echo "$version" > "$TARGET_DIR/.installed-version"
  248. }
  249. ensure_pipx() {
  250. if command -v pipx >/dev/null 2>&1; then
  251. log "✓ pipx is already installed"
  252. PIPX_CMD="pipx"
  253. return
  254. fi
  255. log "pipx not found. Installing pipx..."
  256. # Detect OS and try to install via package manager first (preferred for PEP 668 systems)
  257. local os_type
  258. os_type=$(detect_os)
  259. case "$os_type" in
  260. debian)
  261. log "Installing pipx via apt..."
  262. if sudo apt-get update && sudo apt-get install -y pipx; then
  263. log "✓ pipx installed via apt"
  264. # Ensure pipx path is set up
  265. pipx ensurepath >/dev/null 2>&1 || true
  266. else
  267. warn "Failed to install pipx via apt, trying pip..."
  268. install_pipx_with_pip
  269. fi
  270. ;;
  271. fedora)
  272. log "Installing pipx via dnf..."
  273. if sudo dnf install -y pipx; then
  274. log "✓ pipx installed via dnf"
  275. else
  276. warn "Failed to install pipx via dnf, trying pip..."
  277. install_pipx_with_pip
  278. fi
  279. ;;
  280. arch)
  281. log "Installing pipx via pacman..."
  282. if sudo pacman -S --noconfirm python-pipx; then
  283. log "✓ pipx installed via pacman"
  284. else
  285. warn "Failed to install pipx via pacman, trying pip..."
  286. install_pipx_with_pip
  287. fi
  288. ;;
  289. macos)
  290. if command -v brew >/dev/null 2>&1; then
  291. log "Installing pipx via Homebrew..."
  292. if brew install pipx; then
  293. log "✓ pipx installed via Homebrew"
  294. # Ensure pipx path is set up
  295. pipx ensurepath >/dev/null 2>&1 || true
  296. else
  297. warn "Failed to install pipx via Homebrew, trying pip..."
  298. install_pipx_with_pip
  299. fi
  300. else
  301. log "Homebrew not found, installing pipx via pip..."
  302. install_pipx_with_pip
  303. fi
  304. ;;
  305. *)
  306. # Fallback to pip installation
  307. install_pipx_with_pip
  308. ;;
  309. esac
  310. # Try to find pipx command
  311. if command -v pipx >/dev/null 2>&1; then
  312. PIPX_CMD="pipx"
  313. log "✓ pipx is ready to use"
  314. return
  315. fi
  316. # Check in user bin directory
  317. local user_bin
  318. user_bin="$(python3 -m site --user-base 2>/dev/null)/bin"
  319. if [[ -x "$user_bin/pipx" ]]; then
  320. PIPX_CMD="$user_bin/pipx"
  321. log "✓ Found pipx at $PIPX_CMD"
  322. return
  323. fi
  324. error "pipx installed but not found in PATH. Please add $(python3 -m site --user-base)/bin to your PATH or restart your shell."
  325. }
  326. install_pipx_with_pip() {
  327. log "Installing pipx using pip..."
  328. # Try with --user flag first (works on most systems)
  329. if python3 -m pip install --user pipx 2>/dev/null; then
  330. log "✓ pipx installed via pip --user"
  331. return
  332. fi
  333. # If that fails due to externally-managed-environment, try with --break-system-packages
  334. # (only as a last resort and with clear warning)
  335. warn "System has PEP 668 restrictions. Attempting installation with --break-system-packages..."
  336. if python3 -m pip install --user --break-system-packages pipx 2>/dev/null; then
  337. log "✓ pipx installed (with --break-system-packages)"
  338. return
  339. fi
  340. error "Failed to install pipx. Please install it manually: sudo apt install pipx (Debian/Ubuntu) or pip install --user pipx"
  341. }
  342. pipx_install() {
  343. "${PIPX_CMD}" ensurepath >/dev/null 2>&1 || warn "pipx ensurepath failed; make sure pipx's bin dir is on PATH"
  344. log "Installing/updating boilerplates via pipx"
  345. "${PIPX_CMD}" install --editable --force "$TARGET_DIR"
  346. }
  347. check_current_version() {
  348. if [[ -f "$TARGET_DIR/.installed-version" ]]; then
  349. cat "$TARGET_DIR/.installed-version"
  350. else
  351. echo "unknown"
  352. fi
  353. }
  354. main() {
  355. parse_args "$@"
  356. log "Checking system dependencies..."
  357. # Ensure required tools are available
  358. require_command tar
  359. # Ensure Python3, pip, and pipx are installed
  360. ensure_python3
  361. ensure_pip
  362. TARGET_DIR="$(make_absolute_path)"
  363. # Check if already installed
  364. local current_version
  365. current_version=$(check_current_version)
  366. if [[ "$current_version" != "unknown" ]]; then
  367. log "Currently installed version: $current_version"
  368. fi
  369. download_release "$VERSION"
  370. ensure_pipx
  371. pipx_install
  372. local pipx_info
  373. pipx_info=$("${PIPX_CMD}" list --short 2>/dev/null | grep -E '^boilerplates' || echo "boilerplates (not detected)")
  374. local installed_version
  375. installed_version=$(check_current_version)
  376. cat <<EOF2
  377. ✓ Installation complete!
  378. Version: $installed_version
  379. Location: $TARGET_DIR
  380. pipx environment: $pipx_info
  381. To use the CLI:
  382. boilerplate --help
  383. boilerplate compose list
  384. To update to the latest version:
  385. curl -fsSL https://raw.githubusercontent.com/$REPO_OWNER/$REPO_NAME/main/scripts/install.sh | bash
  386. To install a specific version:
  387. curl -fsSL https://raw.githubusercontent.com/$REPO_OWNER/$REPO_NAME/main/scripts/install.sh | bash -s -- --version v1.0.0
  388. EOF2
  389. }
  390. main "$@"