|
| 1 | +#!/bin/bash -eu |
| 2 | +# SPDX-FileCopyrightText: 2025 Uwe Fechner |
| 3 | +# SPDX-License-Identifier: MIT |
| 4 | +# |
| 5 | +# Install and configure matplotlib for ControlPlots.jl (via PythonCall). |
| 6 | +# |
| 7 | +# Two backends are supported: |
| 8 | +# 1) System Python – uses the matplotlib package installed by Ubuntu/Debian (apt). |
| 9 | +# Fastest option; shares the system Python install. |
| 10 | +# 2) CondaPkg – installs matplotlib into a pixi-managed Conda environment. |
| 11 | +# Self-contained; does not require root / sudo. |
| 12 | +# |
| 13 | +# Both options configure the Qt (qtagg) backend for interactive plot windows. |
| 14 | + |
| 15 | +print_usage() { |
| 16 | + echo "Usage:" |
| 17 | + echo " ./bin/install_controlplots [--system | --conda] [-y | --yes] [-h | --help]" |
| 18 | + echo "" |
| 19 | + echo "Options:" |
| 20 | + echo " --system Use the system Python and matplotlib (Ubuntu/Debian apt)" |
| 21 | + echo " --conda Use CondaPkg (pixi) to install matplotlib" |
| 22 | + echo " -y, --yes Non-interactive; accept defaults" |
| 23 | + echo " -h, --help Show this help message" |
| 24 | +} |
| 25 | + |
| 26 | +_backend="" # "system" or "conda" |
| 27 | +_yes=false |
| 28 | + |
| 29 | +while [[ $# -gt 0 ]]; do |
| 30 | + case $1 in |
| 31 | + -h|--help) |
| 32 | + print_usage |
| 33 | + exit 0 |
| 34 | + ;; |
| 35 | + --system) |
| 36 | + _backend="system" |
| 37 | + shift |
| 38 | + ;; |
| 39 | + --conda) |
| 40 | + _backend="conda" |
| 41 | + shift |
| 42 | + ;; |
| 43 | + -y|--yes) |
| 44 | + _yes=true |
| 45 | + shift |
| 46 | + ;; |
| 47 | + *) |
| 48 | + echo "Unknown option: $1" |
| 49 | + print_usage |
| 50 | + exit 1 |
| 51 | + ;; |
| 52 | + esac |
| 53 | +done |
| 54 | + |
| 55 | +# Always run from the repository root (resolve from this script location). |
| 56 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 57 | +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" |
| 58 | +cd "${REPO_ROOT}" |
| 59 | + |
| 60 | +CONTROLPLOTS_PROJECT="examples_cp" |
| 61 | + |
| 62 | +# ── Interactive backend selection ───────────────────────────────────────────── |
| 63 | + |
| 64 | +if [[ -z "$_backend" ]]; then |
| 65 | + echo "Which matplotlib backend do you want to use for ControlPlots?" |
| 66 | + echo "" |
| 67 | + echo " 1) System Python (uses Ubuntu/Debian python3-matplotlib via apt)" |
| 68 | + echo " - Requires sudo" |
| 69 | + echo " - Shares the system-wide Python installation" |
| 70 | + echo "" |
| 71 | + echo " 2) CondaPkg (installs matplotlib into a pixi-managed Conda environment)" |
| 72 | + echo " - No sudo required" |
| 73 | + echo " - Self-contained; downloads ~200 MB on first use" |
| 74 | + echo "" |
| 75 | + if [[ "$_yes" == true ]]; then |
| 76 | + _choice="2" |
| 77 | + echo "Using default: 2 (CondaPkg)" |
| 78 | + else |
| 79 | + read -rp "Enter 1 or 2 [default: 2]: " _choice |
| 80 | + fi |
| 81 | + case "${_choice:-2}" in |
| 82 | + 1) _backend="system" ;; |
| 83 | + 2) _backend="conda" ;; |
| 84 | + *) |
| 85 | + echo "Invalid choice. Please enter 1 or 2." |
| 86 | + exit 1 |
| 87 | + ;; |
| 88 | + esac |
| 89 | +fi |
| 90 | + |
| 91 | +echo "" |
| 92 | +echo "Selected backend: $_backend" |
| 93 | +echo "" |
| 94 | + |
| 95 | +# ── Helper: install matplotlib via CondaPkg ─────────────────────────────────── |
| 96 | + |
| 97 | +_install_matplotlib_condapkg() { |
| 98 | + echo "Installing matplotlib and pyqt into CondaPkg (pixi) environment..." |
| 99 | + julia --project="${CONTROLPLOTS_PROJECT}" -e ' |
| 100 | +using Pkg |
| 101 | +# Ensure CondaPkg is available |
| 102 | +if Base.find_package("CondaPkg") === nothing |
| 103 | + Pkg.add("CondaPkg") |
| 104 | +end |
| 105 | +using CondaPkg |
| 106 | +CondaPkg.add("matplotlib") |
| 107 | +CondaPkg.add("pyqt") |
| 108 | +CondaPkg.resolve() |
| 109 | +println("matplotlib and pyqt installed in CondaPkg environment.") |
| 110 | +' |
| 111 | +} |
| 112 | + |
| 113 | +_verify_controlplots() { |
| 114 | + echo "Verifying ControlPlots can be loaded..." |
| 115 | + local _julia_prefix="" |
| 116 | + if [[ "$(uname -s)" == "Linux" ]]; then |
| 117 | + _julia_prefix="env -u LD_PRELOAD -u LD_LIBRARY_PATH" |
| 118 | + fi |
| 119 | + if $_julia_prefix julia --project="${CONTROLPLOTS_PROJECT}" -e ' |
| 120 | +using ControlPlots |
| 121 | +println("ControlPlots loaded successfully.") |
| 122 | +'; then |
| 123 | + echo "" |
| 124 | + echo "✓ ControlPlots is working." |
| 125 | + else |
| 126 | + echo "" |
| 127 | + echo "Warning: ControlPlots could not be loaded. Check the error output above." |
| 128 | + echo "You may need to run this script again with the other backend option." |
| 129 | + exit 1 |
| 130 | + fi |
| 131 | +} |
| 132 | + |
| 133 | +# ── Helper: write MPLBACKEND=qtagg to LocalPreferences.toml ────────────────── |
| 134 | + |
| 135 | +_set_mplbackend_qtagg() { |
| 136 | + local _prefs_file="${CONTROLPLOTS_PROJECT}/LocalPreferences.toml" |
| 137 | + # Remove any existing [ENV] section managed by this script. |
| 138 | + if [[ -f "$_prefs_file" ]]; then |
| 139 | + _tmp=$(mktemp) |
| 140 | + awk ' |
| 141 | + /^\[ENV\]/ { in_env=1; next } |
| 142 | + in_env && /^\[/ { in_env=0 } |
| 143 | + !in_env { print } |
| 144 | + ' "$_prefs_file" > "$_tmp" |
| 145 | + mv "$_tmp" "$_prefs_file" |
| 146 | + fi |
| 147 | + { |
| 148 | + echo "" |
| 149 | + echo "[ENV]" |
| 150 | + echo "MPLBACKEND = \"qtagg\"" |
| 151 | + } >> "$_prefs_file" |
| 152 | + echo "Set MPLBACKEND=qtagg in $_prefs_file." |
| 153 | +} |
| 154 | + |
| 155 | +# ── System Python backend ───────────────────────────────────────────────────── |
| 156 | + |
| 157 | +if [[ "$_backend" == "system" ]]; then |
| 158 | + # Detect package manager and install python3-matplotlib if missing. |
| 159 | + if [[ "$(uname -s)" == "Linux" ]]; then |
| 160 | + if grep -qiE "ubuntu|debian" /etc/os-release 2>/dev/null; then |
| 161 | + if ! dpkg -s python3-matplotlib &>/dev/null 2>&1 || ! dpkg -s python3-pyqt5 &>/dev/null 2>&1; then |
| 162 | + echo "Installing python3-matplotlib and python3-pyqt5 via apt..." |
| 163 | + sudo apt install -y python3-matplotlib python3-pyqt5 |
| 164 | + else |
| 165 | + echo "python3-matplotlib and python3-pyqt5 are already installed." |
| 166 | + fi |
| 167 | + elif grep -qi "fedora" /etc/os-release 2>/dev/null; then |
| 168 | + if ! rpm -q python3-matplotlib &>/dev/null 2>&1 || ! rpm -q python3-pyqt5 &>/dev/null 2>&1; then |
| 169 | + echo "Installing python3-matplotlib and python3-qt5 via dnf..." |
| 170 | + sudo dnf install -y python3-matplotlib python3-pyqt5 |
| 171 | + else |
| 172 | + echo "python3-matplotlib and python3-pyqt5 are already installed." |
| 173 | + fi |
| 174 | + else |
| 175 | + echo "Warning: Could not detect Ubuntu/Debian or Fedora." |
| 176 | + echo "Please ensure python3-matplotlib is installed manually before continuing." |
| 177 | + if [[ "$_yes" == false ]]; then |
| 178 | + read -rp "Continue anyway? (y/n) [default: y]: " _cont |
| 179 | + case "${_cont:-y}" in |
| 180 | + n|N) echo "Aborted."; exit 1 ;; |
| 181 | + esac |
| 182 | + fi |
| 183 | + fi |
| 184 | + elif [[ "$(uname -s)" =~ ^(MINGW|MSYS|CYGWIN) ]]; then |
| 185 | + # Windows (Git Bash): use pip to install matplotlib and PyQt5. |
| 186 | + _pip="" |
| 187 | + for _pip_candidate in pip3 pip; do |
| 188 | + if command -v "$_pip_candidate" &>/dev/null; then |
| 189 | + _pip="$_pip_candidate" |
| 190 | + break |
| 191 | + fi |
| 192 | + done |
| 193 | + if [[ -n "$_pip" ]]; then |
| 194 | + if ! "$_pip" show matplotlib &>/dev/null 2>&1 || ! "$_pip" show PyQt5 &>/dev/null 2>&1; then |
| 195 | + echo "Installing matplotlib and PyQt5 via pip..." |
| 196 | + "$_pip" install --user matplotlib PyQt5 |
| 197 | + else |
| 198 | + echo "matplotlib and PyQt5 are already installed." |
| 199 | + fi |
| 200 | + else |
| 201 | + echo "Warning: pip not found. Please install matplotlib and PyQt5 manually:" |
| 202 | + echo " pip install matplotlib PyQt5" |
| 203 | + if [[ "$_yes" == false ]]; then |
| 204 | + read -rp "Continue anyway? (y/n) [default: y]: " _cont |
| 205 | + case "${_cont:-y}" in |
| 206 | + n|N) echo "Aborted."; exit 1 ;; |
| 207 | + esac |
| 208 | + fi |
| 209 | + fi |
| 210 | + else |
| 211 | + echo "Warning: System Python backend is intended for Ubuntu/Debian/Fedora Linux or Windows." |
| 212 | + echo "On this OS ($(uname -s)) you may need to install matplotlib manually." |
| 213 | + if [[ "$_yes" == false ]]; then |
| 214 | + read -rp "Continue anyway? (y/n) [default: y]: " _cont |
| 215 | + case "${_cont:-y}" in |
| 216 | + n|N) echo "Aborted."; exit 1 ;; |
| 217 | + esac |
| 218 | + fi |
| 219 | + fi |
| 220 | + |
| 221 | + # Locate the system python3 executable. |
| 222 | + _syspython="" |
| 223 | + if [[ "$(uname -s)" =~ ^(MINGW|MSYS|CYGWIN) ]]; then |
| 224 | + # On Windows (Git Bash) 'python3' may not exist; also try 'python'. |
| 225 | + for _candidate in python3 python; do |
| 226 | + if command -v "$_candidate" &>/dev/null; then |
| 227 | + _syspython=$(command -v "$_candidate") |
| 228 | + # Verify it runs (guards against the Windows Store stub). |
| 229 | + if "$_syspython" --version &>/dev/null 2>&1; then |
| 230 | + break |
| 231 | + fi |
| 232 | + _syspython="" |
| 233 | + fi |
| 234 | + done |
| 235 | + else |
| 236 | + for _candidate in python3 /usr/bin/python3; do |
| 237 | + if command -v "$_candidate" &>/dev/null; then |
| 238 | + _syspython=$(command -v "$_candidate") |
| 239 | + break |
| 240 | + fi |
| 241 | + done |
| 242 | + fi |
| 243 | + if [[ -z "$_syspython" ]]; then |
| 244 | + echo "Error: python3 not found on PATH. Please install python3." |
| 245 | + exit 1 |
| 246 | + fi |
| 247 | + echo "Found system Python: $_syspython" |
| 248 | + |
| 249 | + # Remove the CondaPkg-managed environment so that PythonCall won't keep |
| 250 | + # using a stale conda Python when switching to system Python. |
| 251 | + if [[ -d "${CONTROLPLOTS_PROJECT}/.CondaPkg" ]]; then |
| 252 | + echo "Removing ${CONTROLPLOTS_PROJECT}/.CondaPkg (switching from CondaPkg to system Python)..." |
| 253 | + rm -rf "${CONTROLPLOTS_PROJECT}/.CondaPkg" |
| 254 | + fi |
| 255 | + # Unset JULIA_PYTHONCALL_EXE so the LocalPreferences.toml exe setting takes |
| 256 | + # effect. |
| 257 | + unset JULIA_PYTHONCALL_EXE |
| 258 | + # Prevent CondaPkg from re-installing a Conda env in the current process. |
| 259 | + export JULIA_CONDAPKG_BACKEND="Null" |
| 260 | + export JULIA_PYTHONCALL_EXE="$_syspython" |
| 261 | + |
| 262 | + # Helper: update a LocalPreferences.toml file with the system Python settings. |
| 263 | + _write_system_python_prefs() { |
| 264 | + local _pf="$1" |
| 265 | + if [[ -f "$_pf" ]]; then |
| 266 | + _tmp=$(mktemp) |
| 267 | + awk ' |
| 268 | + /^\[PythonCall\]/ { in_sec=1; next } |
| 269 | + /^\[PyCall\]/ { in_sec=1; next } |
| 270 | + /^\[CondaPkg\]/ { in_sec=1; next } |
| 271 | + in_sec && /^\[/ { in_sec=0 } |
| 272 | + !in_sec { print } |
| 273 | + ' "$_pf" > "$_tmp" |
| 274 | + mv "$_tmp" "$_pf" |
| 275 | + fi |
| 276 | + { |
| 277 | + echo "" |
| 278 | + echo "[PythonCall]" |
| 279 | + echo "exe = \"$_syspython\"" |
| 280 | + echo "" |
| 281 | + echo "[CondaPkg]" |
| 282 | + echo "backend = \"Null\"" |
| 283 | + } >> "$_pf" |
| 284 | + echo "Written to $_pf." |
| 285 | + } |
| 286 | + |
| 287 | + echo "" |
| 288 | + echo "Saving python_exe=$_syspython to LocalPreferences.toml files..." |
| 289 | + # Write to both the root project and examples_cp project, because |
| 290 | + # `julia --project=examples_cp` reads examples_cp/LocalPreferences.toml, |
| 291 | + # not the root one. |
| 292 | + _write_system_python_prefs "LocalPreferences.toml" |
| 293 | + _write_system_python_prefs "${CONTROLPLOTS_PROJECT}/LocalPreferences.toml" |
| 294 | + |
| 295 | + _set_mplbackend_qtagg |
| 296 | + _verify_controlplots |
| 297 | + |
| 298 | + echo "" |
| 299 | + echo "Done. ControlPlots will use the system Python (PythonCall) matplotlib with the Qt (qtagg) backend." |
| 300 | + |
| 301 | +# ── CondaPkg backend ────────────────────────────────────────────────────────── |
| 302 | + |
| 303 | +elif [[ "$_backend" == "conda" ]]; then |
| 304 | + # Remove [PythonCall] exe, [CondaPkg] backend override, and legacy [PyCall] |
| 305 | + # so PythonCall falls back to the CondaPkg-managed Python. |
| 306 | + # Must be done in both the root and examples_cp LocalPreferences.toml. |
| 307 | + _remove_python_prefs() { |
| 308 | + local _pf="$1" |
| 309 | + [[ -f "$_pf" ]] || return 0 |
| 310 | + _tmp=$(mktemp) |
| 311 | + awk ' |
| 312 | + /^\[PythonCall\]/ { in_sec=1; next } |
| 313 | + /^\[PyCall\]/ { in_sec=1; next } |
| 314 | + /^\[CondaPkg\]/ { in_sec=1; next } |
| 315 | + in_sec && /^\[/ { in_sec=0 } |
| 316 | + !in_sec { print } |
| 317 | + ' "$_pf" > "$_tmp" |
| 318 | + if ! diff -q "$_pf" "$_tmp" &>/dev/null; then |
| 319 | + mv "$_tmp" "$_pf" |
| 320 | + echo "Removed [PythonCall]/[CondaPkg] sections from $_pf (switching to CondaPkg)." |
| 321 | + else |
| 322 | + rm -f "$_tmp" |
| 323 | + fi |
| 324 | + } |
| 325 | + _remove_python_prefs "LocalPreferences.toml" |
| 326 | + _remove_python_prefs "${CONTROLPLOTS_PROJECT}/LocalPreferences.toml" |
| 327 | + |
| 328 | + _install_matplotlib_condapkg |
| 329 | + |
| 330 | + _set_mplbackend_qtagg |
| 331 | + _verify_controlplots |
| 332 | + |
| 333 | + echo "" |
| 334 | + echo "Done. ControlPlots will use the CondaPkg-managed matplotlib with the Qt (qtagg) backend." |
| 335 | +fi |
0 commit comments