setup-deps.sh 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  1. #!/usr/bin/env bash
  2. # Standard-of-Iron — dependency checker and auto-installer
  3. # (Debian/Ubuntu + Arch/Manjaro + macOS/Homebrew)
  4. #
  5. # Verifies required toolchain and Qt/QML runtime modules and installs any missing ones.
  6. # Safe to run multiple times. Requires sudo privileges for Linux installation; Homebrew for macOS.
  7. #
  8. # Usage:
  9. # ./scripts/setup-deps.sh # interactive install as needed
  10. # ./scripts/setup-deps.sh --yes # non-interactive (assume yes)
  11. # ./scripts/setup-deps.sh --dry-run # show actions without installing
  12. # ./scripts/setup-deps.sh --no-install # only verify, do not install
  13. # ./scripts/setup-deps.sh --allow-similar # allow proceeding on similar distros
  14. set -euo pipefail
  15. MIN_CMAKE="3.21.0"
  16. MIN_GXX="10.0.0"
  17. ASSUME_YES=false
  18. DRY_RUN=false
  19. NO_INSTALL=false
  20. ALLOW_SIMILAR=false
  21. for arg in "$@"; do
  22. case "$arg" in
  23. -y|--yes) ASSUME_YES=true ;;
  24. --dry-run) DRY_RUN=true ;;
  25. --no-install) NO_INSTALL=true ;;
  26. --allow-similar) ALLOW_SIMILAR=true ;;
  27. -h|--help)
  28. grep '^#' "$0" | sed -e 's/^# \{0,1\}//'
  29. exit 0
  30. ;;
  31. *) echo "Unknown option: $arg" >&2; exit 2 ;;
  32. esac
  33. done
  34. # -----------------------------------------------------------------------------
  35. # UI helpers
  36. # -----------------------------------------------------------------------------
  37. info() { echo -e "\033[1;34m[i]\033[0m $*"; }
  38. ok() { echo -e "\033[1;32m[✓]\033[0m $*"; }
  39. warn() { echo -e "\033[1;33m[!]\033[0m $*"; }
  40. err() { echo -e "\033[1;31m[x]\033[0m $*"; }
  41. # -----------------------------------------------------------------------------
  42. # Small utilities
  43. # -----------------------------------------------------------------------------
  44. semver_ge() {
  45. # Returns 0 if $1 >= $2, else 1 (supports X.Y[.Z])
  46. # Handle empty or invalid inputs
  47. if [ -z "${1:-}" ] || [ -z "${2:-}" ]; then
  48. return 1
  49. fi
  50. # Ensure inputs are valid version strings (only numbers and dots)
  51. if ! echo "$1" | grep -qE '^[0-9]+(\.[0-9]+)*$' || ! echo "$2" | grep -qE '^[0-9]+(\.[0-9]+)*$'; then
  52. return 1
  53. fi
  54. local IFS=.
  55. local a1 a2 a3 b1 b2 b3
  56. read -r a1 a2 a3 <<< "$1"
  57. read -r b1 b2 b3 <<< "$2"
  58. # Ensure all variables are numeric
  59. a1=${a1:-0}; a2=${a2:-0}; a3=${a3:-0}
  60. b1=${b1:-0}; b2=${b2:-0}; b3=${b3:-0}
  61. # Convert to integers, defaulting to 0 if not numeric
  62. a1=$((a1 + 0)); a2=$((a2 + 0)); a3=$((a3 + 0))
  63. b1=$((b1 + 0)); b2=$((b2 + 0)); b3=$((b3 + 0))
  64. (( a1 > b1 )) && return 0
  65. (( a1 < b1 )) && return 1
  66. (( a2 > b2 )) && return 0
  67. (( a2 < b2 )) && return 1
  68. (( a3 >= b3 )) && return 0
  69. return 1
  70. }
  71. is_macos() { [ "$(uname -s)" = "Darwin" ]; }
  72. read_os_release() {
  73. # Reads /etc/os-release and prints ID, ID_LIKE, and PRETTY_NAME
  74. # shellcheck disable=SC1091
  75. if [ -f /etc/os-release ]; then
  76. (
  77. . /etc/os-release
  78. printf 'ID=%s\n' "${ID:-unknown}"
  79. printf 'ID_LIKE=%s\n' "${ID_LIKE:-}"
  80. printf 'PRETTY_NAME=%s\n' "${PRETTY_NAME:-}"
  81. )
  82. else
  83. printf 'ID=%s\n' "unknown"
  84. printf 'ID_LIKE=%s\n' ""
  85. printf 'PRETTY_NAME=%s\n' ""
  86. fi
  87. }
  88. has_apt() { command -v apt-get >/dev/null 2>&1; }
  89. has_pacman() { command -v pacman >/dev/null 2>&1; }
  90. has_brew() { command -v brew >/dev/null 2>&1; }
  91. is_deb_family_exact() {
  92. case "$1" in
  93. ubuntu|debian|linuxmint|pop) return 0 ;;
  94. *) return 1 ;;
  95. esac
  96. }
  97. is_deb_family_like() {
  98. case " $1 " in
  99. *"debian"*|*"ubuntu"*) return 0 ;;
  100. *) return 1 ;;
  101. esac
  102. }
  103. is_arch_family_exact() {
  104. case "$1" in
  105. arch|manjaro|endeavouros|garuda) return 0 ;;
  106. *) return 1 ;;
  107. esac
  108. }
  109. is_arch_family_like() {
  110. case " $1 " in
  111. *"arch"*) return 0 ;;
  112. *) return 1 ;;
  113. esac
  114. }
  115. detect_distro() {
  116. if is_macos; then
  117. printf 'darwin||macOS\n' # id|like|pretty
  118. return
  119. fi
  120. local id=unknown like= pretty=
  121. while IFS='=' read -r k v; do
  122. case "$k" in
  123. ID) id=${v} ;;
  124. ID_LIKE) like=${v} ;;
  125. PRETTY_NAME) pretty=${v} ;;
  126. esac
  127. done < <(read_os_release)
  128. # strip optional quotes
  129. id=${id%\"}; id=${id#\"}
  130. like=${like%\"}; like=${like#\"}
  131. pretty=${pretty%\"}; pretty=${pretty#\"}
  132. printf '%s|%s|%s\n' "$id" "$like" "$pretty"
  133. }
  134. # -----------------------------------------------------------------------------
  135. # Package maps
  136. # -----------------------------------------------------------------------------
  137. # APT (Debian/Ubuntu) toolchain + GL/Vulkan + formatting tools
  138. APT_PKGS=(
  139. build-essential
  140. cmake
  141. git
  142. pkg-config
  143. clang-format
  144. clang-tidy
  145. libgl1-mesa-dev
  146. mesa-utils
  147. libglx-mesa0
  148. mesa-vulkan-drivers
  149. libegl1
  150. )
  151. # APT Qt6 dev headers / tools
  152. QT6_DEV_PKGS=(
  153. qt6-base-dev
  154. qt6-base-dev-tools
  155. qt6-declarative-dev
  156. qt6-declarative-dev-tools
  157. qt6-tools-dev
  158. qt6-tools-dev-tools
  159. qt6-multimedia-dev
  160. )
  161. # APT Qt6 QML runtime modules
  162. QT6_QML_RUN_PKGS=(
  163. qml6-module-qtqml
  164. qml6-module-qtquick
  165. qml6-module-qtquick-window
  166. qml6-module-qtquick-layouts
  167. qml6-module-qtquick-controls
  168. qml6-module-qtqml-workerscript
  169. qml6-module-qtquick-templates
  170. )
  171. # APT Qt5 fallback (if Qt6 pieces not available)
  172. QT5_QML_RUN_PKGS=(
  173. qml-module-qtqml
  174. qml-module-qtquick2
  175. qml-module-qtquick-window2
  176. qml-module-qtquick-layouts
  177. qml-module-qtquick-controls2
  178. qtbase5-dev
  179. qtdeclarative5-dev
  180. qtdeclarative5-dev-tools
  181. qtmultimedia5-dev
  182. libqt5multimedia5-plugins
  183. qtquickcontrols2-5-dev
  184. )
  185. # PACMAN (Arch/Manjaro) toolchain + GL/Vulkan + formatting tools
  186. PAC_PKGS=(
  187. base-devel
  188. cmake
  189. git
  190. pkgconf
  191. clang
  192. mesa
  193. mesa-demos
  194. libglvnd
  195. vulkan-icd-loader
  196. vulkan-tools
  197. )
  198. # PACMAN Qt6 dev/runtime
  199. QT6_DEV_PAC=(
  200. qt6-base
  201. qt6-declarative
  202. qt6-tools
  203. qt6-shadertools
  204. qt6-multimedia
  205. )
  206. QT6_QML_RUN_PAC=(
  207. qt6-declarative
  208. qt6-quickcontrols2
  209. )
  210. # PACMAN Qt5 fallback
  211. QT5_QML_RUN_PAC=(
  212. qt5-base
  213. qt5-declarative
  214. qt5-quickcontrols2
  215. qt5-multimedia
  216. )
  217. # DNF (Fedora/RHEL) toolchain + GL/Vulkan + formatting tools
  218. DNF_PKGS=(
  219. gcc-c++
  220. make
  221. cmake
  222. git
  223. pkgconf
  224. clang
  225. clang-tools-extra
  226. mesa-libGL-devel
  227. mesa-vulkan-drivers
  228. vulkan-loader
  229. vulkan-tools
  230. )
  231. # DNF Qt6 dev/runtime
  232. QT6_DEV_DNF=(
  233. qt6-qtbase-devel
  234. qt6-qtdeclarative-devel
  235. qt6-qttools-devel
  236. qt6-qtmultimedia-devel
  237. qt6-qtquickcontrols2-devel
  238. )
  239. # DNF Qt5 fallback (if Qt6 not available)
  240. QT5_QML_RUN_DNF=(
  241. qt5-qtbase-devel
  242. qt5-qtdeclarative-devel
  243. qt5-qtquickcontrols2-devel
  244. qt5-qtmultimedia-devel
  245. )
  246. # Homebrew (macOS)
  247. BREW_PKGS=(
  248. cmake
  249. git
  250. pkg-config
  251. llvm # provides clang-format
  252. ninja # nice-to-have for faster CMake builds
  253. )
  254. BREW_QT=( qt ) # keg-only; contains Qt6 base/declarative/tools/Quick Controls 2
  255. BREW_VK=( molten-vk vulkan-loader vulkan-tools )
  256. # -----------------------------------------------------------------------------
  257. # APT helpers
  258. # -----------------------------------------------------------------------------
  259. apt_pkg_available() { apt-cache show "$1" >/dev/null 2>&1; }
  260. filter_available_pkgs_apt() {
  261. local out=() p
  262. for p in "$@"; do
  263. if apt_pkg_available "$p"; then out+=("$p"); fi
  264. done
  265. printf '%s\n' "${out[@]}"
  266. }
  267. dpkg_installed() { dpkg -s "$1" >/dev/null 2>&1; }
  268. apt_update_once() {
  269. if [ "${_APT_UPDATED:-0}" != 1 ]; then
  270. info "Updating apt package lists"
  271. if $DRY_RUN; then
  272. echo "sudo apt-get update"
  273. else
  274. sudo apt-get update
  275. fi
  276. _APT_UPDATED=1
  277. fi
  278. }
  279. apt_install() {
  280. local to_install=() pkg
  281. for pkg in "$@"; do
  282. [ -n "${pkg:-}" ] || continue
  283. if dpkg_installed "$pkg"; then
  284. ok "$pkg already installed"
  285. else
  286. to_install+=("$pkg")
  287. fi
  288. done
  289. if [ ${#to_install[@]} -gt 0 ]; then
  290. if $NO_INSTALL; then
  291. warn "Missing packages: ${to_install[*]} (skipping install due to --no-install)"
  292. return 0
  293. fi
  294. if ! $ASSUME_YES; then
  295. echo
  296. read -r -p "Install missing packages: ${to_install[*]} ? [Y/n] " ans
  297. case "${ans:-Y}" in y|Y) ;; *) warn "User declined install"; return 0 ;; esac
  298. fi
  299. apt_update_once
  300. info "Installing: ${to_install[*]}"
  301. if $DRY_RUN; then
  302. echo "DEBIAN_FRONTEND=noninteractive sudo apt-get install -y ${to_install[*]}"
  303. else
  304. DEBIAN_FRONTEND=noninteractive sudo apt-get install -y "${to_install[@]}"
  305. fi
  306. fi
  307. }
  308. # -----------------------------------------------------------------------------
  309. # PACMAN helpers
  310. # -----------------------------------------------------------------------------
  311. pacman_pkg_available() { pacman -Si "$1" >/dev/null 2>&1 || sudo pacman -Si "$1" >/dev/null 2>&1; }
  312. filter_available_pkgs_pacman() {
  313. local out=() p
  314. for p in "$@"; do
  315. if pacman_pkg_available "$p"; then out+=("$p"); fi
  316. done
  317. printf '%s\n' "${out[@]}"
  318. }
  319. pacman_installed() { pacman -Qi "$1" >/dev/null 2>&1; }
  320. pacman_update_once() {
  321. if [ "${_PAC_UPDATED:-0}" != 1 ]; then
  322. info "Refreshing pacman package databases"
  323. if $DRY_RUN; then
  324. echo "sudo pacman -Sy"
  325. else
  326. sudo pacman -Sy --noconfirm
  327. fi
  328. _PAC_UPDATED=1
  329. fi
  330. }
  331. pacman_install() {
  332. local to_install=() pkg
  333. for pkg in "$@"; do
  334. [ -n "${pkg:-}" ] || continue
  335. if pacman_installed "$pkg"; then
  336. ok "$pkg already installed"
  337. else
  338. to_install+=("$pkg")
  339. fi
  340. done
  341. if [ ${#to_install[@]} -gt 0 ]; then
  342. if $NO_INSTALL; then
  343. warn "Missing packages: ${to_install[*]} (skipping install due to --no-install)"
  344. return 0
  345. fi
  346. if ! $ASSUME_YES; then
  347. echo
  348. read -r -p "Install missing packages (pacman): ${to_install[*]} ? [Y/n] " ans
  349. case "${ans:-Y}" in y|Y) ;; *) warn "User declined install"; return 0 ;; esac
  350. fi
  351. pacman_update_once
  352. info "Installing: ${to_install[*]}"
  353. local args=(-S --needed)
  354. $ASSUME_YES && args+=(--noconfirm)
  355. if $DRY_RUN; then
  356. echo "sudo pacman ${args[*]} ${to_install[*]}"
  357. else
  358. sudo pacman "${args[@]}" "${to_install[@]}"
  359. fi
  360. fi
  361. }
  362. # -----------------------------------------------------------------------------
  363. # DNF helpers (Fedora/RHEL)
  364. # -----------------------------------------------------------------------------
  365. dnf_pkg_available() { dnf info "$1" >/dev/null 2>&1; }
  366. filter_available_pkgs_dnf() {
  367. local out=() p
  368. for p in "$@"; do
  369. if dnf_pkg_available "$p"; then out+=("$p"); fi
  370. done
  371. printf '%s\n' "${out[@]}"
  372. }
  373. dnf_installed() { rpm -q "$1" >/dev/null 2>&1; }
  374. dnf_update_once() {
  375. if [ "${_DNF_UPDATED:-0}" != 1 ]; then
  376. info "Refreshing DNF package metadata"
  377. if $DRY_RUN; then
  378. echo "sudo dnf makecache"
  379. else
  380. sudo dnf makecache -y
  381. fi
  382. _DNF_UPDATED=1
  383. fi
  384. }
  385. dnf_install() {
  386. local to_install=() pkg
  387. for pkg in "$@"; do
  388. [ -n "${pkg:-}" ] || continue
  389. if dnf_installed "$pkg"; then
  390. ok "$pkg already installed"
  391. else
  392. to_install+=("$pkg")
  393. fi
  394. done
  395. if [ ${#to_install[@]} -gt 0 ]; then
  396. if $NO_INSTALL; then
  397. warn "Missing packages: ${to_install[*]} (skipping install due to --no-install)"
  398. return 0
  399. fi
  400. if ! $ASSUME_YES; then
  401. echo
  402. read -r -p "Install missing packages (dnf): ${to_install[*]} ? [Y/n] " ans
  403. case "${ans:-Y}" in y|Y) ;; *) warn "User declined install"; return 0 ;; esac
  404. fi
  405. dnf_update_once
  406. info "Installing: ${to_install[*]}"
  407. local args=(-y install)
  408. if $DRY_RUN; then
  409. echo "sudo dnf ${args[*]} ${to_install[*]}"
  410. else
  411. sudo dnf "${args[@]}" "${to_install[@]}"
  412. fi
  413. fi
  414. }
  415. # -----------------------------------------------------------------------------
  416. # BREW helpers (macOS)
  417. # -----------------------------------------------------------------------------
  418. brew_installed() { brew list --versions "$1" >/dev/null 2>&1; }
  419. brew_update_once() {
  420. if [ "${_BREW_UPDATED:-0}" != 1 ]; then
  421. info "Updating Homebrew formulae"
  422. if $DRY_RUN; then
  423. echo "brew update"
  424. else
  425. brew update
  426. fi
  427. _BREW_UPDATED=1
  428. fi
  429. }
  430. brew_install() {
  431. local to_install=() pkg
  432. for pkg in "$@"; do
  433. [ -n "${pkg:-}" ] || continue
  434. if brew_installed "$pkg"; then
  435. ok "$pkg already installed"
  436. else
  437. to_install+=("$pkg")
  438. fi
  439. done
  440. if [ ${#to_install[@]} -gt 0 ]; then
  441. if $NO_INSTALL; then
  442. warn "Missing formulae: ${to_install[*]} (skipping install due to --no-install)"
  443. return 0
  444. fi
  445. if ! $ASSUME_YES; then
  446. echo
  447. read -r -p "Install missing formulae (brew): ${to_install[*]} ? [Y/n] " ans
  448. case "${ans:-Y}" in y|Y) ;; *) warn "User declined install"; return 0 ;; esac
  449. fi
  450. brew_update_once
  451. info "Installing (brew): ${to_install[*]}"
  452. if $DRY_RUN; then
  453. echo "brew install ${to_install[*]}"
  454. else
  455. brew install "${to_install[@]}"
  456. fi
  457. fi
  458. }
  459. post_brew_qt_note() {
  460. # Qt is keg-only; ensure users have it on PATH or CMake can find it
  461. local qp
  462. qp=$(brew --prefix qt 2>/dev/null || true)
  463. if [ -n "$qp" ] && ! command -v qmake >/dev/null 2>&1; then
  464. info "Qt is keg-only on Homebrew. Ensure CMake can find it. Options:"
  465. echo " - Add to PATH: export PATH=\"$qp/bin:\$PATH\""
  466. echo " - Or: brew link --overwrite --force qt (not generally recommended)"
  467. echo " - CMake hints: -DQt6_DIR=\"$qp/lib/cmake/Qt6\""
  468. fi
  469. local lp
  470. lp=$(brew --prefix llvm 2>/dev/null || true)
  471. if [ -n "$lp" ] && ! command -v clang-format >/dev/null 2>&1; then
  472. warn "clang-format from Homebrew LLVM isn't on PATH."
  473. echo " Add to PATH: export PATH=\"$lp/bin:\$PATH\""
  474. fi
  475. }
  476. # -----------------------------------------------------------------------------
  477. # Checks
  478. # -----------------------------------------------------------------------------
  479. check_tool_versions() {
  480. info "Checking toolchain versions"
  481. # cmake
  482. if command -v cmake >/dev/null 2>&1; then
  483. local cmv
  484. cmv=$(cmake --version | head -n1 | awk '{print $3}')
  485. if semver_ge "$cmv" "$MIN_CMAKE"; then
  486. ok "cmake $cmv (>= $MIN_CMAKE)"
  487. else
  488. warn "cmake $cmv (< $MIN_CMAKE)"
  489. fi
  490. else
  491. warn "cmake not found"
  492. fi
  493. # C++ compiler (prefer g++, then clang++)
  494. local CXX_BIN=""
  495. if command -v g++ >/dev/null 2>&1; then
  496. CXX_BIN="g++"
  497. elif command -v clang++ >/dev/null 2>&1; then
  498. CXX_BIN="clang++"
  499. fi
  500. if [ -z "$CXX_BIN" ]; then
  501. warn "No C++ compiler found (g++ or clang++)."
  502. else
  503. local gxv
  504. if [ "$CXX_BIN" = "g++" ]; then
  505. gxv=$(g++ --version | head -n1 | awk '{print $NF}')
  506. if semver_ge "$gxv" "$MIN_GXX"; then
  507. ok "g++ $gxv (>= $MIN_GXX)"
  508. else
  509. warn "g++ $gxv (< $MIN_GXX)"
  510. fi
  511. else
  512. # clang++ version parsing (Apple/LLVM format)
  513. gxv=$($CXX_BIN --version | head -n1 | sed -E 's/.*version ([0-9]+(\.[0-9]+){0,2}).*/\1/')
  514. ok "clang++ $gxv (Apple/LLVM) — accepted"
  515. fi
  516. fi
  517. # git
  518. if command -v git >/dev/null 2>&1; then
  519. ok "git $(git --version | awk '{print $3}')"
  520. else
  521. warn "git not found"
  522. fi
  523. # pkg-config or pkgconf
  524. if command -v pkg-config >/dev/null 2>&1; then
  525. ok "pkg-config $(pkg-config --version)"
  526. elif command -v pkgconf >/dev/null 2>&1; then
  527. ok "pkgconf $(pkgconf --version)"
  528. else
  529. warn "Neither pkg-config nor pkgconf found"
  530. fi
  531. # clang-tidy
  532. if command -v clang-tidy >/dev/null 2>&1; then
  533. ok "clang-tidy $(clang-tidy --version | head -n1)"
  534. else
  535. warn "clang-tidy not found — recommended for static analysis"
  536. fi
  537. }
  538. # -----------------------------------------------------------------------------
  539. # Installers
  540. # -----------------------------------------------------------------------------
  541. install_runtime_apt() {
  542. info "Installing base toolchain (APT)"
  543. apt_install "${APT_PKGS[@]}"
  544. info "Installing Qt6 SDK/dev packages (APT)"
  545. mapfile -t _qt6dev < <(filter_available_pkgs_apt "${QT6_DEV_PKGS[@]}")
  546. apt_install "${_qt6dev[@]}"
  547. info "Installing Qt6 QML runtime modules (APT)"
  548. mapfile -t _qt6qml < <(filter_available_pkgs_apt "${QT6_QML_RUN_PKGS[@]}")
  549. apt_install "${_qt6qml[@]}"
  550. info "Installing Qt5 QML runtime modules (fallback, if available; APT)"
  551. mapfile -t _qt5qml < <(filter_available_pkgs_apt "${QT5_QML_RUN_PKGS[@]}")
  552. apt_install "${_qt5qml[@]}"
  553. }
  554. install_runtime_pacman() {
  555. info "Installing base toolchain (pacman)"
  556. pacman_install "${PAC_PKGS[@]}"
  557. info "Installing Qt6 SDK/dev packages (pacman)"
  558. mapfile -t _qt6dev < <(filter_available_pkgs_pacman "${QT6_DEV_PAC[@]}")
  559. pacman_install "${_qt6dev[@]}"
  560. info "Ensuring Qt6 QML runtime (pacman)"
  561. mapfile -t _qt6qml < <(filter_available_pkgs_pacman "${QT6_QML_RUN_PAC[@]}")
  562. pacman_install "${_qt6qml[@]}"
  563. info "Installing Qt5 QML runtime (fallback, if available; pacman)"
  564. mapfile -t _qt5qml < <(filter_available_pkgs_pacman "${QT5_QML_RUN_PAC[@]}")
  565. pacman_install "${_qt5qml[@]}"
  566. }
  567. install_runtime_dnf() {
  568. info "Installing base toolchain (dnf)"
  569. dnf_install "${DNF_PKGS[@]}"
  570. info "Installing Qt6 SDK/dev packages (dnf)"
  571. mapfile -t _qt6dev < <(filter_available_pkgs_dnf "${QT6_DEV_DNF[@]}")
  572. dnf_install "${_qt6dev[@]}"
  573. info "Installing Qt5 fallback (if available; dnf)"
  574. mapfile -t _qt5qml < <(filter_available_pkgs_dnf "${QT5_QML_RUN_DNF[@]}")
  575. dnf_install "${_qt5qml[@]}"
  576. }
  577. install_runtime_brew() {
  578. if ! has_brew; then
  579. err "Homebrew is required on macOS. Install from https://brew.sh and re-run."
  580. exit 1
  581. fi
  582. info "Installing base toolchain (brew)"
  583. brew_install "${BREW_PKGS[@]}"
  584. info "Installing Qt6 (brew)"
  585. brew_install "${BREW_QT[@]}"
  586. info "Installing Vulkan (optional; brew)"
  587. brew_install "${BREW_VK[@]}"
  588. post_brew_qt_note
  589. }
  590. # -----------------------------------------------------------------------------
  591. # Main
  592. # -----------------------------------------------------------------------------
  593. main() {
  594. local id like pretty
  595. IFS='|' read -r id like pretty <<< "$(detect_distro)"
  596. info "Detected system: ${pretty:-$id} (ID=$id; ID_LIKE='${like:-}')."
  597. local pm=""
  598. if [ "$id" = "darwin" ]; then
  599. pm="brew"; info "macOS detected (Homebrew)."
  600. elif is_deb_family_exact "$id"; then
  601. pm="apt"; info "Exact Debian/Ubuntu family detected ($id)."
  602. elif is_arch_family_exact "$id"; then
  603. pm="pacman"; info "Exact Arch/Manjaro family detected ($id)."
  604. elif [ "$id" = "fedora" ] || [[ "${like:-}" == *"rhel"* ]] || command -v dnf >/dev/null 2>&1; then
  605. pm="dnf"; info "Fedora or RHEL-compatible system detected."
  606. elif is_deb_family_like "${like:-}" && has_apt; then
  607. pm="apt"
  608. warn "No exact match, but this system is *similar* to Debian/Ubuntu and has apt-get."
  609. if $ALLOW_SIMILAR || $ASSUME_YES; then
  610. info "Proceeding with Debian/Ubuntu-compatible steps due to --allow-similar/--yes."
  611. else
  612. echo
  613. read -r -p "Proceed using Debian/Ubuntu package set on this similar distro? [Y/n] " ans
  614. case "${ans:-Y}" in y|Y) info "Continuing with Debian/Ubuntu-compatible steps." ;;
  615. *) err "User declined proceeding on a similar distro."; exit 1 ;; esac
  616. fi
  617. elif is_arch_family_like "${like:-}" && has_pacman; then
  618. pm="pacman"
  619. warn "No exact match, but this system is *similar* to Arch and has pacman."
  620. if $ALLOW_SIMILAR || $ASSUME_YES; then
  621. info "Proceeding with Arch-compatible steps due to --allow-similar/--yes."
  622. else
  623. echo
  624. read -r -p "Proceed using Arch/Manjaro package set on this similar distro? [Y/n] " ans
  625. case "${ans:-Y}" in y|Y) info "Continuing with Arch-compatible steps." ;;
  626. *) err "User declined proceeding on a similar distro."; exit 1 ;; esac
  627. fi
  628. else
  629. if has_apt; then
  630. pm="apt"
  631. warn "Unknown distro '$id', but apt-get is present."
  632. $ALLOW_SIMILAR || $ASSUME_YES || {
  633. read -r -p "Proceed using apt-based steps? (may or may not work) [y/N] " ans
  634. case "${ans:-N}" in y|Y) ;; *) err "Exiting. Use --allow-similar to override."; exit 1 ;; esac
  635. }
  636. elif has_pacman; then
  637. pm="pacman"
  638. warn "Unknown distro '$id', but pacman is present."
  639. $ALLOW_SIMILAR || $ASSUME_YES || {
  640. read -r -p "Proceed using pacman-based steps? (may or may not work) [y/N] " ans
  641. case "${ans:-N}" in y|Y) ;; *) err "Exiting. Use --allow-similar to override."; exit 1 ;; esac
  642. }
  643. else
  644. err "No supported package manager found (apt-get or pacman). On macOS, install Homebrew."
  645. echo "Required (roughly):"
  646. echo " - build tools (gcc/clang), cmake >= $MIN_CMAKE, git, pkg-config"
  647. echo " - OpenGL/Vulkan runtime (system or MoltenVK),"
  648. echo " - Qt6 base + declarative + tools + Quick Controls 2 (Qt5 fallback ok)."
  649. exit 1
  650. fi
  651. fi
  652. # Install first, then verify versions (so fresh systems don't bail early).
  653. case "$pm" in
  654. apt) install_runtime_apt ;;
  655. pacman) install_runtime_pacman ;;
  656. dnf) install_runtime_dnf ;;
  657. brew) install_runtime_brew ;;
  658. *) err "Internal error: unknown package manager '$pm'"; exit 1 ;;
  659. esac
  660. echo
  661. check_tool_versions
  662. echo
  663. ok "All required dependencies should now be present."
  664. echo "- cmake >= $MIN_CMAKE"
  665. echo "- C++ compiler (g++ >= $MIN_GXX or clang++)"
  666. echo "- Qt 6 base + declarative + tools + QML runtime modules (Qt5 QML fallback if available)"
  667. echo "- clang-format (for C/C++ and shader formatting)"
  668. echo "- qmlformat (via Qt dev tools, if available)"
  669. }
  670. main "$@"