|
#!/usr/bin/env bash |
|
#=============================================================================== |
|
# NAME: Thonny + py5mode Linux Installer |
|
# REQUIREMENT: Python 3.10+, pip, internet connection |
|
# USAGE: ./thonny-installer.bash |
|
# NOTES: Add exec flag 1st: chmod +x ./thonny-installer.bash |
|
# DESCRIPTION: Installs both Thonny IDE + py5mode plugin + py5 full package |
|
# GIST_REPO: https://Gist.GitHub.com/GoToLoop/246a31d437aaa8c6eadb7f7186544e0f |
|
# SUPPORT: https://GitHub.com/py5coding/thonny-py5mode/issues |
|
# AUTHOR: GoToLoop ( https://Discourse.Processing.org/c/28 ) |
|
# VERSION: 1.2.9 |
|
# CREATED: 2025-Sep-02 |
|
# UPDATED: 2026-Feb-14 |
|
# LICENSE: MIT |
|
#=============================================================================== |
|
|
|
set -euo pipefail; echo |
|
|
|
VERSION=1.2.9 # Version to echo on console for flag --version |
|
VENV_DIR=""; PYTHON="" # Target venv & python respectively |
|
PYTHON_DEFAULT=python # Default Python binary name |
|
LOCAL_SHARE=${XDG_DATA_HOME:-$HOME/.local/share} # Cross-Desktop Group |
|
ARGS=("$@"); LEN=${#ARGS[@]} # Array of all user's passed args and its length |
|
|
|
# Function to parse individual command-line arguments: |
|
parse_argument() { |
|
local key=$1; local val=$2 |
|
|
|
case $key in # Check for named arg |
|
--help|-h|'/h'|'/?') # Summary of available flags |
|
echo -e "Usage: $0 [OPTIONS] [VENV_DIR] [PYTHON]\n" |
|
echo "Options:" |
|
echo " --venv, -v, /v Path to the virtual environment directory" |
|
echo " --python, -p, /p Path or name of an existing Python binary" |
|
echo " --version, -V, /V Show current script version and exit" |
|
echo " --help, -h, /h, /? Show this help message and exit" |
|
exit 0 ;; |
|
|
|
--version|-V|'/V') echo $VERSION; exit 0 ;; # Display version and exit |
|
|
|
--venv|-v|'/v') VENV_DIR=$val ;; # Venv target installation folder |
|
|
|
--python|-p|'/p') PYTHON=$val ;; # Python binary path or name |
|
|
|
*) # Positional arg fallback |
|
if [ -z "$VENV_DIR" ]; then VENV_DIR=$key |
|
elif [ -z "$PYTHON" ]; then PYTHON=$key |
|
fi |
|
esac |
|
} |
|
|
|
# Parse user arguments (named or positional): |
|
for (( i=0; i<LEN; ++i )); do |
|
key=${ARGS[i]}; val="" |
|
|
|
if [[ $key == *=* ]]; then # Check if current arg contains `=` |
|
val=${key#*=}; key=${key%%=*} # Handle --flag=value or -f=value |
|
elif (( i + 1 < LEN )); then # Check if it hasn't already reached last arg |
|
val=${ARGS[++i]} # Next arg is the value if key doesn't contain `=` |
|
fi |
|
|
|
parse_argument "$key" "$val" |
|
done |
|
|
|
# Check if pypi.org pip repo is remotely reachable: |
|
if ! ping -4c 1 pypi.org &> /dev/null; then |
|
echo -e "⚠️ Warning: pypi.org is unreachable! Package install may fail.\n" |
|
fi |
|
|
|
# Search the system for existing Python binaries if none provided by argument: |
|
if [ -z "$PYTHON" ]; then |
|
# Get system-wide Python commands from $PATH: |
|
mapfile -t SYSTEM_PY < <( |
|
compgen -c | grep -E '(^|[-_])python([0-9\.]*)?$' | sort -V | uniq |
|
) |
|
|
|
# Get pyenv-installed Python binaries: |
|
PYENV_FOLDER=${PYENV_ROOT:-$HOME/.pyenv}/versions |
|
mapfile -t PYENV_PY < <( |
|
find "$PYENV_FOLDER" -type f -executable \ |
|
-regex '.*/bin/python[0-9\.]*$' 2>/dev/null |
|
) |
|
|
|
# Get uv-installed Python binaries: |
|
UV_FOLDER="$LOCAL_SHARE/uv/python" |
|
mapfile -t UV_PY < <( |
|
find "$UV_FOLDER" -type f -executable \ |
|
-regex '.*/bin/python[0-9\.]*$' 2>/dev/null |
|
) |
|
|
|
# Combine, deduplicate and validate: |
|
PYTHON_CANDIDATES=("${SYSTEM_PY[@]}" "${PYENV_PY[@]}" "${UV_PY[@]}") |
|
VALID_PYTHONS=() # Initialize array to store valid Python versions |
|
|
|
for candidate in "${PYTHON_CANDIDATES[@]}"; do # Validate each candidate |
|
command -v "$candidate" &> /dev/null && VALID_PYTHONS+=("$candidate") |
|
done |
|
|
|
# List available Python versions and their index: |
|
if [ ${#VALID_PYTHONS[@]} -gt 0 ]; then # if any valid Python versions found |
|
echo "Available Python interpreters: 🐍" |
|
|
|
for i in "${!VALID_PYTHONS[@]}"; do |
|
candidate=${VALID_PYTHONS[$i]} |
|
label=system |
|
|
|
case "$candidate" in |
|
"$PYENV_FOLDER"*) label=pyenv ;; |
|
"$UV_FOLDER"*) label=uv |
|
esac |
|
|
|
echo " [$i] $candidate ($label)" |
|
done |
|
|
|
msg="\n📝 You can enter a full path, a Python name," |
|
echo -e "$msg or just an index number from the list above.\n" |
|
|
|
PYTHON_DEFAULT=${VALID_PYTHONS[0]} # Pick 1st valid Python version found |
|
fi |
|
fi |
|
|
|
while [ -z "$PYTHON" ]; do # Only prompt for Python if not set via user args... |
|
# Ask for Python executable (default: index [0] "python"): |
|
read -rei "$PYTHON_DEFAULT" -p "🐍 Python executable path or index: " PYTHON |
|
done |
|
|
|
[[ $PYTHON == ~* ]] && PYTHON=${PYTHON/#\~/$HOME} # Expand tilde ~ |
|
|
|
# If input is a valid index, convert it to the actual path: |
|
[[ $PYTHON =~ ^[0-9]+$ ]] && ((PYTHON >= 0 && |
|
PYTHON < ${#VALID_PYTHONS[@]})) && PYTHON=${VALID_PYTHONS[$PYTHON]} |
|
echo -e "✅ Chosen Python for creating venv: $PYTHON\n" |
|
|
|
# Validate the final Python path: |
|
if ! (command -v "$PYTHON" &> /dev/null && "$PYTHON" -V &> /dev/null); then |
|
echo "❌ Invalid Python path: $PYTHON"; exit 1 |
|
fi |
|
|
|
while [ -z "$VENV_DIR" ]; do # Only prompt for venv if not set via user args... |
|
# Ask for venv target folder (default: ~/Apps/Thonny/): |
|
read -rei "$HOME/Apps/Thonny" -p "📁 Virtualenv target folder: " VENV_DIR |
|
done |
|
|
|
[[ $VENV_DIR == ~* ]] && VENV_DIR=${VENV_DIR/#\~/$HOME} # Expand tilde ~ |
|
[ "$VENV_DIR" != / ] && VENV_DIR=${VENV_DIR%/} # Remove trailing / |
|
|
|
# Extract the parent directory of the virtual environment target: |
|
VENV_DIR_PARENT=$(dirname "$VENV_DIR") |
|
|
|
# Define a regex that matches either the root (/) or the user's home directory: |
|
REGEX="^(/|${HOME})$" |
|
|
|
# Block installation if the target path or its parent is either / or $HOME |
|
# This prevents accidental installs into critical system or personal folders: |
|
if [[ $VENV_DIR =~ $REGEX || $VENV_DIR_PARENT =~ $REGEX ]]; then |
|
echo -e "\n❌ Direct home/root subfolder not allowed: $VENV_DIR" |
|
exit 1 |
|
fi |
|
|
|
# Check if target already exists: |
|
if [ -e "$VENV_DIR" ]; then |
|
echo |
|
|
|
read -rp "⚠️ Warning: '$VENV_DIR' already exists. Overwrite? [y/N] " YES |
|
[[ ${YES:0:1} =~ ^[YySsOo]$ ]] || { echo "❌ Aborting..."; exit 1; } |
|
|
|
rm -rf "$VENV_DIR" # Delete target folder before creating venv there |
|
fi |
|
|
|
# Create the virtual environment: |
|
echo -e "\n🛠 Creating virtualenv in '$VENV_DIR' using '$PYTHON'..." |
|
"$PYTHON" -m venv --copies "$VENV_DIR" |
|
|
|
# Upgrade venv's pip and install Thonny + py5: |
|
echo -e "📦 Installing thonny + thonny-py5mode...\n" |
|
VENV_BIN="$VENV_DIR/bin"; VENV_PIP="$VENV_BIN/pip" |
|
"$VENV_PIP" install -U pip; echo |
|
"$VENV_PIP" install thonny thonny-py5mode[extras]; echo |
|
|
|
# Also, add package pip-review for easily update everything inside venv. |
|
# To use it, while venv is active, type in: pip-review -aC |
|
"$VENV_PIP" install pip-review; echo |
|
|
|
# Locate Thonny icon inside the virtual environment's site-packages folder, |
|
# regardless of the specific Python version (e.g., python3.10, python3.13). |
|
# This finds the first matching "thonny.png" file and stores its full path: |
|
ICON_PATH=$(find "$VENV_DIR/lib" -type f \ |
|
-path "*/site-packages/thonny/res/thonny.png" | head -n 1) |
|
|
|
# Check if previous icon search resulted in an empty string: |
|
if [ -z "$ICON_PATH" ]; then |
|
echo "🔶 Warning: Thonny icon not found!" |
|
ICON_PATH="/usr/share/icons/hicolor/256x256/apps/org.thonny.Thonny.png" |
|
[ -f $ICON_PATH ] || ICON_PATH=thonny # Try 'thonny' as last recourse. |
|
|
|
# If not empty, locally register the found Thonny's icon: |
|
elif command -v xdg-icon-resource &> /dev/null; then |
|
echo -e "🖼️ Registering Thonny icon as 'thonny' from: $ICON_PATH\n" |
|
xdg-icon-resource install --size 256 --novendor "$ICON_PATH" thonny |
|
ICON_PATH=thonny # Change icon to the locally registered name 'thonny'. |
|
fi |
|
|
|
# Build KDE's ".directory" icon for Thonny's env folder: |
|
cat > "$VENV_DIR/.directory" <<EOF |
|
[Desktop Entry] |
|
Icon=$ICON_PATH |
|
EOF |
|
|
|
# Make sure "Desktop" folder exists: |
|
DESKTOP_DIR="$HOME/Desktop" |
|
mkdir -p "$DESKTOP_DIR" |
|
|
|
# Build "Thonny.desktop" launcher: |
|
THONNY_LAUNCHER="$DESKTOP_DIR/Thonny.desktop"; THONNY_EXE="$VENV_BIN/thonny" |
|
cat > "$THONNY_LAUNCHER" <<EOF |
|
[Desktop Entry] |
|
Type=Application |
|
Name=Thonny |
|
GenericName=Python IDE |
|
Comment=Run Thonny IDE in isolated environment |
|
Exec=$THONNY_EXE %F |
|
Icon=$ICON_PATH |
|
Terminal=false |
|
StartupWMClass=Thonny |
|
Categories=Development;IDE |
|
Keywords=programming;education |
|
MimeType=text/x-python |
|
Actions=Edit |
|
|
|
[Desktop Action Edit] |
|
Exec=$THONNY_EXE %F |
|
Name=Edit with Thonny |
|
EOF |
|
|
|
# Make the "Thonny.desktop" launcher file executable: |
|
chmod +x "$THONNY_LAUNCHER" |
|
|
|
if command -v gio &> /dev/null; then |
|
gio set "$THONNY_LAUNCHER" metadata::trusted true # Make the launcher trusted |
|
fi |
|
|
|
# And also make a copy of "Thonny.desktop" inside Thonny's env folder: |
|
cp -p "$THONNY_LAUNCHER" "$VENV_DIR" |
|
|
|
# Build "run-thonny" CLI launcher, using relative path to find Thonny's Python: |
|
THONNY_CLI_RUN="$VENV_DIR/run-thonny" |
|
|
|
# Quoting 'EOF' prevents variable expansion within the heredoc operator `<<`. |
|
# Those variables will expand only when "run-thonny" script runs: |
|
cat > "$THONNY_CLI_RUN" <<'EOF' |
|
#!/usr/bin/env bash |
|
|
|
# Get the absolute path to the directory containing this running script: |
|
SCRIPT_FOLDER="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
|
|
|
# Construct the path to the Python executable inside the virtual environment: |
|
PYTHON_EXE="$SCRIPT_FOLDER/bin/python" |
|
|
|
# Define the folder where Thonny's log file will be stored: |
|
LOG_FOLDER="$SCRIPT_FOLDER/.thonny" |
|
|
|
# Define the full path to the log file: |
|
LOG_FILE="$LOG_FOLDER/thonny.log" |
|
|
|
# Create the log directory if it doesn't already exist: |
|
mkdir -p "$LOG_FOLDER" |
|
|
|
# Launch Thonny module in the background using the virtual environment's Python. |
|
# Redirect both stdout and stderr to the log file. |
|
# `nohup` allows the process to continue running after the terminal is closed. |
|
# `disown` detaches the process from the shell's job control table: |
|
nohup "$PYTHON_EXE" -m thonny > "$LOG_FILE" 2>&1 & disown |
|
EOF |
|
|
|
# Make the "run-thonny" CLI launcher file executable: |
|
chmod +x "$THONNY_CLI_RUN" |
|
|
|
# List all installed packages: |
|
"$VENV_PIP" list; echo |
|
|
|
# Show further details of the 3 main installed packages: |
|
"$VENV_PIP" show thonny thonny-py5mode py5; echo |
|
|
|
echo -e "✅ Setup complete! You can now launch Thonny from your desktop.\n" |
|
|
|
# Check default shell and show the corresponding instruction to activate venv: |
|
case "$SHELL" in |
|
*/fish) |
|
echo "🐟 You're using Fish. Run the command below to activate venv:" |
|
echo "source $VENV_BIN/activate.fish" ;; |
|
*) |
|
echo "💡 Using Bash or a compatible shell. Run this to activate venv:" |
|
echo "source $VENV_BIN/activate" |
|
esac |