#!/usr/bin/env bash # # QSWAT+ installer / updater for Linux and macOS (per-user). # # curl -fsSL https://install.swat.tamu.edu/qsp.sh | bash # # Installs the QSWATPlus QGIS plugin into every QGIS profile you choose, plus the # SWAT+ support directory (Linux: ~/.local/share/SWATPlus, macOS: ~/SWATPlus) holding # the Databases and a self-contained TauDEM (its own GDAL + MPICH bundled, so no # system GDAL/MPI is needed). The plugin ships compiled extensions for several # Python/OS/arch combinations side by side; QGIS loads the one matching its Python # automatically. # # qsp.sh install or update (asks which QGIS profiles + the soils DB) # qsp.sh --uninstall remove the plugin from all profiles and the support dir # qsp.sh --all install into every detected profile without asking # qsp.sh --editor also install the SWAT+ Editor (delegates to spe.sh) # qsp.sh --with-soils also download the soils reference DB (no prompt) # qsp.sh --no-soils skip the soils DB (no prompt) # qsp.sh --check only check the requirements; install nothing, exit 1 if one is not met # # The soils reference database (~40 MB) is optional; you are asked at install time. # Pass --with-soils / --no-soils (or SWATPLUS_SOILS=yes|no) to answer without the prompt. # (The WGN database is installed by the SWAT+ Editor, not here.) # set -euo pipefail # Download mirrors, tried in order: install.swat.tamu.edu, then install.swatplus.link. BASES=( "https://install.swat.tamu.edu/downloads" "https://install.swatplus.link/downloads" ) FEED="qswatplus" # subdir under each mirror base PLUGIN_NAME="QSWATPlus" # installed plugin folder / QGIS id PLUGIN_TGZ="$FEED/qswatplus.tgz" # arch-independent (all .so coexist) EXAMPLES_TGZ="$FEED/exampledatasets.tgz" # arch-independent example datasets SUPPORT_DIR="$HOME/.local/share/SWATPlus" DB_DIR="$SUPPORT_DIR/Databases" BIN_DIR="$HOME/.local/bin" EDITOR_URL="https://install.swat.tamu.edu/spe.sh" # Optional, large reference databases live at each mirror root (not in the support # payload). Downloaded only on request. SOILS_ZIP="swatplus_soils.zip" # ~40 MB -> Databases/swatplus_soils.sqlite WANT_SOILS="${SWATPLUS_SOILS:-ask}" say() { printf '\033[1;36m==>\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m!! \033[0m %s\n' "$*" >&2; } die() { printf '\033[1;31mxx \033[0m %s\n' "$*" >&2; exit 1; } GOOD_BASE="" # mirror that last served a valid file; tried first next time sha256() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1 else shasum -a 256 "$1" | cut -d' ' -f1; fi } # _looks_valid [expected-sha] : a 200 can be a soft-404 HTML page, so # validate the content matches what we asked for. _looks_valid() { local kind="$1" f="$2" exp="${3:-}" [ -s "$f" ] || return 1 case "$kind" in gz) head -c2 "$f" | od -An -tx1 | tr -d ' \n' | grep -qi '^1f8b' ;; zip) head -c2 "$f" | grep -q 'PK' ;; sqlite) head -c16 "$f" | od -An -tx1 | tr -d ' \n' | grep -qi '^53514c69746520666f726d6174203300' ;; sha256) grep -qiE '^[0-9a-f]{64}' "$f" ;; version) grep -qE '^v?[0-9]+\.[0-9]+' "$f" ;; sha) [ "$(sha256 "$f")" = "$exp" ] ;; *) return 0 ;; esac } hbytes() { awk -v b="${1:-0}" 'BEGIN{ if (b>=1073741824) printf "%.1fG", b/1073741824; else if (b>=1048576) printf "%.0fM", b/1048576; else if (b>=1024) printf "%.0fK", b/1024; else printf "%dB", b; }' } _fsize() { stat -c %s "$1" 2>/dev/null || stat -f %z "$1" 2>/dev/null || echo 0; } _bar_width() { local cols w; cols="$(tput cols 2>/dev/null || echo "${COLUMNS:-80}")" w=$(( cols - 22 )); [ "$w" -gt 28 ] && w=28; [ "$w" -lt 8 ] && w=8; printf '%s' "$w" } _draw_bar() { local done="$1" total="$2" w pct fill bar; w="$(_bar_width)" pct=$(( done * 100 / total )); [ "$pct" -gt 100 ] && pct=100 fill=$(( done * w / total )); [ "$fill" -gt "$w" ] && fill="$w" bar="$(printf '%*s' "$fill" '' | tr ' ' '#')" printf '\r [%-*s] %3d%% %s ' "$w" "$bar" "$pct" "$(hbytes "$done")" >&2 } download_show() { # local url="$1" out="$2" total pid rc total="$(curl -fsSLI --connect-timeout 8 --proto '=https' --tlsv1.2 "$url" 2>/dev/null \ | awk 'tolower($1)=="content-length:"{v=$2} END{gsub(/[\r ]/,"",v); print v}')" if { [ -t 2 ] || [ -t 1 ]; } && [ -n "$total" ] && [ "$total" -ge 1 ] 2>/dev/null; then curl -fSL -s --connect-timeout 8 --retry 3 --retry-delay 2 --proto '=https' --tlsv1.2 "$url" -o "$out" & pid=$! while kill -0 "$pid" 2>/dev/null; do _draw_bar "$(_fsize "$out")" "$total"; sleep 0.2; done if wait "$pid"; then rc=0; else rc=$?; fi [ "$rc" -eq 0 ] && _draw_bar "$total" "$total"; printf '\n' >&2 return "$rc" fi printf ' downloading %s (%s) ...\n' "$(basename "$url")" "$(hbytes "${total:-0}")" >&2 curl -fSL -s --connect-timeout 8 --retry 3 --retry-delay 2 --proto '=https' --tlsv1.2 "$url" -o "$out" } # fetch [expected-sha] [progress] fetch() { local rel="$1" out="$2" kind="$3" exp="${4:-}" prog="${5:-}" base order=() if [ -n "$GOOD_BASE" ]; then order=("$GOOD_BASE"); for base in "${BASES[@]}"; do [ "$base" = "$GOOD_BASE" ] || order+=("$base"); done else order=("${BASES[@]}"); fi for base in "${order[@]}"; do if [ "$prog" = progress ]; then download_show "$base/$rel" "$out" || continue else curl -fsSL --connect-timeout 8 --retry 3 --retry-delay 2 --proto '=https' --tlsv1.2 "$base/$rel" -o "$out" 2>/dev/null || continue; fi if _looks_valid "$kind" "$out" "$exp"; then GOOD_BASE="$base"; return 0; fi done return 1 } # fetch a tarball and verify it against its published .sha256 (if available). fetch_verified_tgz() { # local rel="$1" out="$2" exp="" if fetch "$rel.sha256" "$TMP/sha" sha256; then exp="$(grep -oiE '^[0-9a-f]{64}' "$TMP/sha" | head -1 || true)"; fi fetch "$rel" "$out" gz "" progress || return 1 if [ -n "$exp" ]; then [ "$(sha256 "$out")" = "$exp" ] || { warn "checksum mismatch for $(basename "$rel")"; return 1; } say "checksum OK ($(basename "$rel"))" fi return 0 } # True only when /dev/tty can actually be opened (a real terminal is attached). # `[ -r /dev/tty ]` is not enough: the node can exist yet fail to open under curl|bash. tty_ok() { { true /dev/null; } ask() { # -> returns 0 for yes; reads from the terminal even under curl|bash local p="$1" a="" if tty_ok; then read -r -p "$p [y/N] " a =2 (gwflow). A clean QGIS install # often lacks them -- notably on Linux distros where QGIS runs on the system Python # (e.g. Arch) -- which makes the plugin fail to load. Check the Python QGIS will use # and stop early with install instructions instead of leaving a half-working plugin. # Bypass with QSWAT_SKIP_DEPCHECK=1. # The Pythons QGIS runs, one per line. Linux: the system python3. macOS: one per # QGIS app in /Applications; current builds (3.44, 4.0) have # Contents/MacOS/python3., older ones Contents/MacOS/bin/python3. The system # /usr/bin/python3 on macOS is not QGIS's Python (and without the Command Line # Tools it is only a stub), so it is never used here. qgis_pythons() { case "$OS" in Linux) command -v python3 2>/dev/null || true ;; Darwin) local app c for app in /Applications/QGIS*.app; do [ -d "$app" ] || continue for c in "$app"/Contents/MacOS/python3.[0-9]* "$app"/Contents/MacOS/bin/python3 \ "$app"/Contents/Resources/python/bin/python3; do case "$c" in *-config) continue ;; esac [ -f "$c" ] && [ -x "$c" ] && { printf '%s\n' "$c"; break; } done done ;; esac return 0 } # PYTHONHOME for one of those Pythons, empty when it needs none. The # Contents/MacOS/python3. of a QGIS app starts only with its home set to the # app's Contents/Frameworks, where lib/python3. is. qgis_pythonhome() { local py="$1" app case "$py" in */Contents/MacOS/python3.*) app="${py%/Contents/MacOS/*}" [ -d "$app/Contents/Frameworks/lib/$(basename "$py")" ] && printf '%s\n' "$app/Contents/Frameworks" ;; esac return 0 } linux_dep_cmd() { local id="" like="" if [ -r /etc/os-release ]; then . /etc/os-release; id="${ID:-}"; like="${ID_LIKE:-}"; fi case " ${id} ${like} " in *" arch "*|*manjaro*|*endeavouros*) echo "sudo pacman -S --needed python-matplotlib python-shapely" ;; *debian*|*ubuntu*|*mint*|*pop*) echo "sudo apt install python3-matplotlib python3-shapely" ;; *fedora*|*rhel*|*centos*|*rocky*|*alma*) echo "sudo dnf install python3-matplotlib python3-shapely" ;; *suse*) echo "sudo zypper install python3-matplotlib python3-Shapely" ;; *) echo "install your distribution's python3-matplotlib and python3-shapely packages" ;; esac } check_python_deps() { [ "${QSWAT_SKIP_DEPCHECK:-}" = 1 ] && { warn "Skipping dependency check (QSWAT_SKIP_DEPCHECK=1)."; return 0; } local pys py home out miss np pin missing=0 unrun=0 pys="$(qgis_pythons)" # With --check, anything this check cannot verify counts as a failure. if [ -z "$pys" ]; then if [ "$OS" = Darwin ]; then [ "$CHECK" -eq 1 ] && die "QGIS not found in /Applications. Install QGIS first, then re-run." warn "No QGIS app found in /Applications to verify QSWAT+ dependencies (matplotlib, shapely); skipping check." else [ "$CHECK" -eq 1 ] && die "python3 not found, so the QSWAT+ dependencies (matplotlib, shapely) cannot be checked." warn "No Python found to verify QSWAT+ dependencies (matplotlib, shapely); skipping check." fi return 0 fi cat > "$TMP/depcheck.py" <<'PY' m = [] try: import matplotlib # noqa: F401 except Exception: m.append("matplotlib") try: import shapely parts = (shapely.__version__.split(".") + ["0", "0"])[:2] if tuple(int(x) for x in parts) < (2, 0): m.append("shapely>=2.0 (found %s)" % shapely.__version__) except Exception: m.append("shapely") print("missing:" + ", ".join(m)) try: import numpy print("numpy:" + numpy.__version__) except Exception: pass PY while IFS= read -r py; do [ -n "$py" ] || continue home="$(qgis_pythonhome "$py")" # `|| true`: a Python that cannot run (a stub, a licence prompt) must not end # the installer here under set -e; it is reported and skipped instead if [ -n "$home" ]; then out="$(PYTHONHOME="$home" "$py" "$TMP/depcheck.py" 2>/dev/null || true)" else out="$("$py" "$TMP/depcheck.py" 2>/dev/null || true)"; fi case "$out" in missing:*) miss="$(printf '%s\n' "$out" | sed -n 's/^missing://p')" ;; *) warn "Could not run $py to verify QSWAT+ dependencies (matplotlib, shapely); skipping it."; unrun=1; continue ;; esac np="$(printf '%s\n' "$out" | sed -n 's/^numpy://p')" if [ -z "$miss" ]; then say "QSWAT+ Python dependencies present (matplotlib, shapely) in $py" continue fi missing=1 warn "QSWAT+ needs Python packages QGIS does not bundle. Missing: $miss" printf ' (checked: %s)\n' "$py" >&2 if [ "$OS" = Darwin ]; then # --user: the app bundle is signed and often not writable. numpy pinned to # QGIS's: the user site comes first on sys.path, and a newer numpy there would # replace the one QGIS's GDAL bindings are built against. pin=""; [ -n "$np" ] && pin=" \"numpy==$np\"" if [ -n "$home" ]; then printf ' Install into this QGIS Python:\n PYTHONHOME="%s" "%s" -m pip install --user matplotlib "shapely>=2"%s\n' "$home" "$py" "$pin" >&2 else printf ' Install into this QGIS Python:\n "%s" -m pip install --user matplotlib "shapely>=2"%s\n' "$py" "$pin" >&2 fi fi done <<< "$pys" if [ "$missing" -eq 0 ]; then [ "$CHECK" -eq 1 ] && [ "$unrun" -eq 1 ] && die "Could not check the QSWAT+ dependencies in every QGIS Python (see above)." return 0 fi if [ "$OS" = Linux ]; then printf ' Install them, then re-run this installer:\n %s\n' "$(linux_dep_cmd)" >&2 die "Missing required Python packages (set QSWAT_SKIP_DEPCHECK=1 to install anyway)." elif [ "$CHECK" -eq 1 ]; then die "Missing required Python packages; install them as shown above, then re-run." else warn "Continuing, but QSWAT+ graphing/gwflow will fail until these are installed." fi } # True when QGIS is installed: an app in /Applications on macOS; on Linux a qgis # command, the Snap or the Flatpak. qgis_found() { case "$OS" in Darwin) ls -d /Applications/QGIS*.app > /dev/null 2>&1 ;; *) command -v qgis > /dev/null 2>&1 || [ -x /snap/bin/qgis ] \ || { command -v flatpak > /dev/null 2>&1 && flatpak info org.qgis.qgis > /dev/null 2>&1; } ;; esac } # --------------------------------------------------------------------------- TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT command -v curl >/dev/null 2>&1 || die "curl is required but not installed." command -v tar >/dev/null 2>&1 || die "tar is required but not installed." # --- OS + architecture -> support dir, QGIS profile roots, payload --------- # Linux: profiles under ~/.local/share (+ flatpak/snap), support in ~/.local/share/SWATPlus. # macOS: profiles under ~/Library/Application Support, support in ~/SWATPlus (where QSWAT+ looks). OS="$(uname -s)" case "$OS" in Linux) SUPPORT_DIR="$HOME/.local/share/SWATPlus" PROFILE_ROOTS=( "$HOME/.local/share/QGIS" "$HOME/.var/app/org.qgis.qgis/data/QGIS" "$HOME/snap/qgis/current/.local/share/QGIS" ) DEFAULT_PROFILE="$HOME/.local/share/QGIS/QGIS3/profiles/default/python/plugins" case "$(uname -m)" in x86_64|amd64) ARCH=x86_64 ;; aarch64|arm64) ARCH=aarch64 ;; *) die "Unsupported architecture: $(uname -m). Supported: x86_64, aarch64." ;; esac ;; Darwin) SUPPORT_DIR="$HOME/SWATPlus" PROFILE_ROOTS=( "$HOME/Library/Application Support/QGIS" ) DEFAULT_PROFILE="$HOME/Library/Application Support/QGIS/QGIS3/profiles/default/python/plugins" case "$(uname -m)" in arm64) ARCH=mac-arm64 ;; x86_64) ARCH=mac-x86_64 ;; *) die "Unsupported macOS architecture: $(uname -m)." ;; esac ;; *) die "Unsupported operating system: $OS (Linux and macOS only)." ;; esac DB_DIR="$SUPPORT_DIR/Databases" EXAMPLES_DIR="$SUPPORT_DIR/ExampleDatasets" SUPPORT_PARENT="$(dirname "$SUPPORT_DIR")" SUPPORT_TGZ="$FEED/swatplus-${ARCH}.tgz" # --- discover QGIS profile plugin directories ------------------------------ # Any QGIS major (QGIS3/QGIS4) and any profile name (not just 'default'). discover_profiles() { local r d for r in "${PROFILE_ROOTS[@]}"; do [ -d "$r" ] || continue for d in "$r"/QGIS*/profiles/*/ ; do [ -d "$d" ] || continue printf '%s\n' "${d%/}/python/plugins" done done } uninstall() { say "Removing the $PLUGIN_NAME plugin from all detected QGIS profiles" local removed=0 p while IFS= read -r p; do [ -n "$p" ] || continue if [ -d "$p/$PLUGIN_NAME" ]; then rm -rf "$p/$PLUGIN_NAME"; say " removed $p/$PLUGIN_NAME"; removed=$((removed+1)); fi done < <(discover_profiles) [ "$removed" -eq 0 ] && warn "no installed $PLUGIN_NAME plugin found" if [ -d "$SUPPORT_DIR" ]; then if ask "Also remove the SWAT+ support data and TauDEM at $SUPPORT_DIR (Databases, TauDEM5Bin)?"; then rm -rf "$SUPPORT_DIR/Databases" "$SUPPORT_DIR/TauDEM5Bin" rmdir "$SUPPORT_DIR" 2>/dev/null || true say " removed support data" else say " kept $SUPPORT_DIR" fi fi say "Done." exit 0 } _norm() { case "$1" in 1|y|Y|yes|Yes|YES|true|TRUE) echo yes ;; 0|n|N|no|No|NO|false|FALSE) echo no ;; *) echo ask ;; esac; } WANT_SOILS="$(_norm "$WANT_SOILS")" ALL=0; WITH_EDITOR=0; CHECK=0 for a in "$@"; do case "$a" in --uninstall) uninstall ;; --all) ALL=1 ;; --editor) WITH_EDITOR=1 ;; --with-soils) WANT_SOILS=yes ;; --no-soils) WANT_SOILS=no ;; --check) CHECK=1 ;; -h|--help) sed -n '2,25p' "$0" 2>/dev/null; exit 0 ;; esac done say "QSWAT+ installer ($OS, $ARCH)" # Stop early if the Python QGIS uses is missing packages QSWAT+ needs (matplotlib, shapely). check_python_deps # --check (used by all.sh before it installs anything): QGIS is installed, the # packages above are there, and the mirrors have a package for this platform. if [ "$CHECK" -eq 1 ]; then qgis_found || die "QGIS not found. Install QGIS first, then re-run." fetch "$SUPPORT_TGZ.sha256" "$TMP/support.sha256" sha256 \ || die "No QSWAT+ package for $OS $(uname -m) on any mirror." say "QSWAT+ requirements met." exit 0 fi # --- choose target profiles ------------------------------------------------- # (read into an array without mapfile, which macOS bash 3.2 lacks) PROFILES=() while IFS= read -r _line; do [ -n "$_line" ] && PROFILES+=("$_line"); done < <(discover_profiles) TARGETS=() if [ "${#PROFILES[@]}" -eq 0 ]; then default="$DEFAULT_PROFILE" warn "No existing QGIS profile found." if [ "$ALL" -eq 1 ] || ask "Create and install into the default QGIS profile ($default)?"; then TARGETS+=("$default") else die "Nothing to install into. Start QGIS once to create a profile, then re-run." fi else say "Detected ${#PROFILES[@]} QGIS profile(s):" for p in "${PROFILES[@]}"; do printf ' - %s\n' "$(printf '%s' "$p" | sed "s#$HOME#~#")"; done if [ "$ALL" -eq 1 ]; then TARGETS=("${PROFILES[@]}") elif ! tty_ok; then say "Non-interactive: installing into all detected profiles." TARGETS=("${PROFILES[@]}") else for p in "${PROFILES[@]}"; do ask "Install QSWAT+ into $(printf '%s' "$p" | sed "s#$HOME#~#") ?" && TARGETS+=("$p") done [ "${#TARGETS[@]}" -eq 0 ] && die "No profile selected." fi fi # --- download payloads ------------------------------------------------------ say "Downloading the QSWAT+ plugin" fetch_verified_tgz "$PLUGIN_TGZ" "$TMP/plugin.tgz" || die "Could not download the plugin from any mirror." # sanity: the archive's top-level dir must be the plugin name. List into a temp # file first; `tar | head` would SIGPIPE-kill tar under `set -o pipefail`. tar tzf "$TMP/plugin.tgz" > "$TMP/plugin.list" 2>/dev/null || true top="$(head -1 "$TMP/plugin.list" | cut -d/ -f1 || true)" [ "$top" = "$PLUGIN_NAME" ] || die "Unexpected plugin archive layout (top dir: '$top')." say "Downloading the SWAT+ support data + TauDEM ($ARCH)" HAVE_SUPPORT=1 fetch_verified_tgz "$SUPPORT_TGZ" "$TMP/support.tgz" || { warn "Support data not available for $ARCH; installing the plugin only."; HAVE_SUPPORT=0; } # --- install plugin into each target --------------------------------------- for p in "${TARGETS[@]}"; do mkdir -p "$p" rm -rf "$p/$PLUGIN_NAME" tar -xzf "$TMP/plugin.tgz" -C "$p" [ "$OS" = Darwin ] && xattr -dr com.apple.quarantine "$p/$PLUGIN_NAME" 2>/dev/null || true say "Installed plugin -> $(printf '%s' "$p/$PLUGIN_NAME" | sed "s#$HOME#~#")" done # --- enable the plugin in each profile ------------------------------------- # QGIS loads a Python plugin at start when its key under [PythonPlugins] in the # profile's settings file is true. QGIS rewrites that file when it exits, so it is # changed only while QGIS is closed. qgis_running() { command -v pgrep >/dev/null 2>&1 && pgrep -xi 'qgis.*' >/dev/null 2>&1; } # enable_plugin_setting : sets QSWATPlus=true under [PythonPlugins] # and echoes created, enabled, unchanged or failed. Only that line changes; every # other byte (UTF-8 text, CRLF or LF, order, a missing final newline) stays, and a # file that already has QSWATPlus=true is not written. enable_plugin_setting() { local ini="$1" state crlf=0 lastnl=0 if [ ! -e "$ini" ]; then mkdir -p "$(dirname "$ini")" && printf '[PythonPlugins]\nQSWATPlus=true\n' > "$ini" || { echo failed; return 0; } echo created; return 0 fi # 2: the key is true; 1: the key has another value; 0: the section has no key; # -1: no [PythonPlugins] section state="$(LC_ALL=C awk ' { l = $0; sub(/\r$/, "", l) } l ~ /^\[/ { if (insec) exit; insec = (l == "[PythonPlugins]"); if (insec) sec = 1; next } insec && l ~ /^[ \t]*QSWATPlus[ \t]*=/ { v = l; sub(/^[^=]*=[ \t]*/, "", v); sub(/[ \t]*$/, "", v); key = (v == "true") ? 2 : 1; exit } END { print (key ? key : (sec ? 0 : -1)) }' "$ini" 2>/dev/null)" || { echo failed; return 0; } [ "$state" = 2 ] && { echo unchanged; return 0; } LC_ALL=C grep -q "$(printf '\r')\$" "$ini" && crlf=1 [ -z "$(tail -c 1 "$ini")" ] && lastnl=1 LC_ALL=C awk -v state="$state" -v crlf="$crlf" -v lastnl="$lastnl" ' BEGIN { eol = crlf ? "\r\n" : "\n" } NR == FNR { n++; next } { l = $0; cr = ""; if (sub(/\r$/, "", l)) cr = "\r" out = $0 if (l ~ /^\[/) insec = (!done && l == "[PythonPlugins]") else if (insec && state == 1 && l ~ /^[ \t]*QSWATPlus[ \t]*=/) { out = "QSWATPlus=true" cr; done = 1; insec = 0 } term = (FNR == n && !lastnl) ? "" : "\n" printf "%s%s", out, term if (insec && state == 0 && l == "[PythonPlugins]") { if (term == "") printf "%s", eol printf "QSWATPlus=true%s", eol; done = 1; insec = 0 } } END { if (state == -1) { if (n > 0 && !lastnl) printf "%s", eol if (n > 0) printf "%s", eol printf "[PythonPlugins]%sQSWATPlus=true%s", eol, eol } }' "$ini" "$ini" > "$ini.qsp-tmp" 2>/dev/null \ && cat "$ini.qsp-tmp" > "$ini" || { rm -f "$ini.qsp-tmp"; echo failed; return 0; } rm -f "$ini.qsp-tmp" echo enabled } ENABLED=0 if qgis_running; then warn "QGIS is running, so QSWAT+ was not enabled. Enable it in Plugins -> Manage and Install Plugins, or close QGIS and run this installer again." else ENABLED=1 for p in "${TARGETS[@]}"; do prof="${p%/python/plugins}" major="$(printf '%s' "$prof" | sed -n 's#.*/QGIS\([0-9][0-9]*\)/profiles/[^/]*$#\1#p')" [ -n "$major" ] || { ENABLED=0; continue; } case "$OS" in Darwin) ini="$prof/qgis.org/QGIS$major.ini" ;; *) ini="$prof/QGIS/QGIS$major.ini" ;; esac case "$(enable_plugin_setting "$ini")" in unchanged) say "QSWAT+ already enabled in $(printf '%s' "$ini" | sed "s#$HOME#~#")" ;; created|enabled) say "Enabled QSWAT+ in $(printf '%s' "$ini" | sed "s#$HOME#~#")" ;; *) warn "Could not enable QSWAT+ in $ini; enable it in Plugins -> Manage and Install Plugins."; ENABLED=0 ;; esac done fi # --- install support data --------------------------------------------------- if [ "$HAVE_SUPPORT" -eq 1 ]; then mkdir -p "$SUPPORT_PARENT" tar -xzf "$TMP/support.tgz" -C "$SUPPORT_PARENT" # ensure TauDEM binaries + bundled mpiexec are executable if [ -d "$SUPPORT_DIR/TauDEM5Bin" ]; then find "$SUPPORT_DIR/TauDEM5Bin" -maxdepth 1 -type f -exec chmod +x {} + 2>/dev/null || true # macOS: clear the quarantine flag so Gatekeeper allows the bundled (ad-hoc signed) binaries [ "$OS" = Darwin ] && xattr -dr com.apple.quarantine "$SUPPORT_DIR/TauDEM5Bin" 2>/dev/null || true fi say "Installed support data -> $(printf '%s' "$SUPPORT_DIR" | sed "s#$HOME#~#")" fi # --- reference datasets database -------------------------------------------- # Pull the datasets DB fresh from the mirror rather than trusting the copy bundled # in the support tarball, so its schema always matches the current tools. This # overwrites the extracted copy. say "Downloading the SWAT+ datasets database" mkdir -p "$DB_DIR" if fetch "4.0/swatplus_datasets.sqlite" "$TMP/datasets.sqlite" sqlite; then mv -f "$TMP/datasets.sqlite" "$DB_DIR/swatplus_datasets.sqlite" say "Installed datasets database -> $(printf '%s' "$DB_DIR/swatplus_datasets.sqlite" | sed "s#$HOME#~#")" else warn "Could not download the datasets database from any mirror; kept the copy from the support package." fi # --- example datasets (Robit, Mary_Creek) -> ExampleDatasets, beside Databases --- say "Downloading the example datasets" if fetch_verified_tgz "$EXAMPLES_TGZ" "$TMP/examples.tgz"; then mkdir -p "$EXAMPLES_DIR" tar -xzf "$TMP/examples.tgz" -C "$EXAMPLES_DIR" say "Installed example datasets -> $(printf '%s' "$EXAMPLES_DIR" | sed "s#$HOME#~#")" else warn "Example datasets not available from any mirror; skipping." fi # --- optional: soils reference database ------------------------------------- ask_db() { #