Skip to content

Commit 1251a17

Browse files
committed
Add install_controlplots
1 parent 289c4b1 commit 1251a17

1 file changed

Lines changed: 335 additions & 0 deletions

File tree

bin/install_controlplots

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
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

Comments
 (0)