Skip to content

Instantly share code, notes, and snippets.

@GoToLoop
Last active March 8, 2026 22:17
Show Gist options
  • Select an option

  • Save GoToLoop/246a31d437aaa8c6eadb7f7186544e0f to your computer and use it in GitHub Desktop.

Select an option

Save GoToLoop/246a31d437aaa8c6eadb7f7186544e0f to your computer and use it in GitHub Desktop.
Bash script to install Thonny + py5mode on Linux

Remote installation command for Windows:

iwr -useb https://Gist.GitHubUserContent.com/GoToLoop/246a31d437aaa8c6eadb7f7186544e0f/raw/thonny-installer.ps1 | iex

or

iwr https://Gist.GitHubUserContent.com/GoToLoop/246a31d437aaa8c6eadb7f7186544e0f/raw/thonny-installer.ps1 -OutFile thonny-installer.ps1; Unblock-File thonny-installer.ps1; Set-ExecutionPolicy Bypass -Scope Process -Force; .\thonny-installer.ps1

Remote installation command for Linux/Unix:

curl -fsSL https://Gist.GitHubUserContent.com/GoToLoop/246a31d437aaa8c6eadb7f7186544e0f/raw/thonny-installer.bash -o thonny-installer.bash && chmod +x thonny-installer.bash && ./thonny-installer.bash

Installation tip:

Just hit ENTER to accept default for Python interpreter & folder install path when prompted by the script.

Relevant discussion link:

https://GitHub.com/py5coding/thonny-py5mode/discussions/77

#!/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
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Installs Thonny IDE, py5mode plugin, and py5 package on Windows.
.DESCRIPTION
This script installs Thonny IDE and the py5 coding mode plugin within a
dedicated Python virtual environment. It handles finding a suitable Python
installation, creating the virtual environment via its venv module, installing
necessary packages via pip, and setting up a shortcut for launching Thonny.
REQUIREMENTS:
* Python 3.10+ installed and accessible via the system's PATH.
* pip + venv (usually installed with Python).
* Internet connection to download packages from PyPI.
* PowerShell terminal to run this script.
USAGE:
.\thonny-installer.ps1 [-VenvDir <string>] [-PythonPath <string>]
[-Help] [-Version]
.PARAMETER VenvDir
Directory to install the virtual environment.
.PARAMETER PythonPath
Path or name of an existing Python binary.
.PARAMETER Help
Show instructions.
.PARAMETER Version
Show script version.
.OUTPUTS
String. Status messages and paths to created resources.
.EXAMPLE
.\thonny-installer.ps1
# Installs using default prompts for Python path and venv directory.
.EXAMPLE
.\thonny-installer.ps1 "C:\MyVenvs\Thonny"
# Installs inside specified target folder and prompts for Python path or name.
.EXAMPLE
.\thonny-installer.ps1 -PythonPath python3.10
# Uses python3.10 from PATH and prompts for venv target directory.
.EXAMPLE
.\thonny-installer.ps1 "C:\MyVenvs\Thonny" "C:\Python310\python.exe"
# Installs inside specified target folder using specified Python path.
.NOTES
NAME: Thonny + py5mode Windows Installer
AUTHOR: GoToLoop
VERSION: 1.0.2
CREATED: 2025-Oct-27
UPDATED: 2026-Mar-08
LICENSE: MIT
- The script searches for 'python.exe' or 'python3.exe' in the system PATH.
- Prompts the user to choose a Python interpreter or type in a path for one.
- The virtual environment defaults to '$HOME\Apps\Thonny'.
- Creates a '.bat' launcher script and a desktop shortcut (.lnk) for Thonny.
.LINK
https://Discourse.Processing.org/c/28 (Forum)
.LINK
https://GitHub.com/py5coding/thonny-py5mode/issues (Support)
.LINK
https://Gist.GitHub.com/GoToLoop/246a31d437aaa8c6eadb7f7186544e0f (Download)
#>
[CmdletBinding()]
param(
[Parameter(HelpMessage = "Target folder for the Python virtual environment.")]
[Alias('venv')]$VenvDir = '',
[Parameter(HelpMessage = "Path or command name for the Python executable.")]
[Alias('p', 'python')]$PythonPath = '',
[Parameter(HelpMessage = "Display this help message and exit.")]
[Alias('h', '?')][switch]$Help,
[Parameter(HelpMessage = "Display script version and exit.")]
[Alias('v')][switch]$Version
)
# --- Configuration ---
$ScriptVersion = "1.0.2"
$DefaultVenvDir = Join-Path $HOME "Apps" "Thonny"
$MinPy = "3.10"
$MinVer = [version]$MinPy
$PyExe = @("python", "python.exe")[$IsWindows]
$ThonnyExe = @("thonny", "thonny.exe")[$IsWindows]
$ErrorActionPreference = "Stop" # Stop on errors
# --- Helper Functions ---
# Function to write errors and exit
function ThrowError([string]$message) {
Write-Host "[Error] $message" -ForegroundColor Red; exit 1
}
# Function to write warnings
function WriteWarning([string]$message) {
Write-Host "[WARN] $message" -ForegroundColor Yellow
}
# Function to write success messages
function WriteSuccess([string]$message) {
Write-Host "[OK] $message" -ForegroundColor Green
}
# Function to write informational messages
function WriteInfo([string]$message) {
Write-Host "[INFO] $message" -ForegroundColor Cyan
}
# Function to execute a command, stopping on error if needed
function Start-Command([string]$cmd, [string[]]$list, [string]$err) {
<#
.SYNOPSIS
Executes a command and throws an error if it fails.
.DESCRIPTION
Runs an external command with specified arguments using Start-Process,
waits for completion, and checks the exit code. Throws a custom error
on non-zero exit codes. Provides success feedback via Write-Host.
.PARAMETER cmd
The executable file path or command to run.
.PARAMETER list
Array of arguments to pass to the command.
.PARAMETER err
Custom error message prefix for the thrown exception.
.EXAMPLE
Start-Command "python" "-m pip install thonny" "Thonny install failed"
.EXAMPLE
Start-Command "gh" @("repo", "clone", "account/repo") "Git clone failed"
#>
$process = Start-Process `
-FilePath $cmd -ArgumentList $list `
-Wait -PassThru -NoNewWindow
if ($process.ExitCode -ne 0) {
ThrowError "$err (Command: $cmd $list, Exit Code: $($process.ExitCode))"
}
Write-Host " -> '$cmd $list' completed successfully."
}
# Function to create a Windows shortcut (.lnk)
function New-Shortcut {
param(
[string]$ShortcutPath, [string]$TargetFile,
$Arguments = "", $WorkingDirectory = "",
$IconLocation = "", $Description = "Launch Thonny IDE"
)
try {
$Shell = New-Object -ComObject WScript.Shell
$Shortcut = $Shell.CreateShortcut($ShortcutPath)
$Shortcut.TargetPath = $TargetFile
$Shortcut.Arguments = $Arguments
$Shortcut.Description = $Description
if ($WorkingDirectory) { $Shortcut.WorkingDirectory = $WorkingDirectory }
# Try to find an icon associated with the target (Thonny executable)
if ($IconLocation) { $Shortcut.IconLocation = $IconLocation }
elseif (Test-Path $TargetFile) { $Shortcut.IconLocation = "$TargetFile,0" }
$Shortcut.Save()
WriteInfo " -> Created shortcut: $ShortcutPath"
} catch {
$msg = "Failed to create shortcut '$ShortcutPath': "
return WriteWarning $msg + $_.Exception.Message
}
# Make shortcut accessible (optional, usually works by default)
(Get-Item $ShortcutPath).IsNetwork = $false
(Get-Item $ShortcutPath).Attributes = 'Normal'
# Mark as trusted if possible (difficult to automate reliably cross-version)
# Try using a .NET method if available, otherwise skip.
try {
Add-Type -AssemblyName System.Management.Automation
New-Object System.IO.FileInfo($ShortcutPath)
} catch {
$msg = "Could not add type for shortcut trust operation: "
Write-Warning $msg + $_.Exception.Message
}
}
# --- Argument Parsing & Initial Setup ---
Write-Host "`n🚀 Thonny + py5mode Windows Installer 🚀"
Write-Host "=================================================="
# Handle -Help and -Version flags and then exit
# Display help using PowerShell's comment-based help
if ($Help) { Get-Help $MyInvocation.MyCommand.Path -Full }
if ($Version) { Write-Host "`nInstaller Version: $ScriptVersion`n" }
if ($Help -or $Version) { exit 0 }
# --- Check Network Connectivity ---
Write-Host "Checking internet connection to PyPI..."
if (-not (Test-Connection -ComputerName pypi.org -Count 1 -Quiet)) {
WriteWarning "pypi.org seems unreachable. Package installation might fail."
} else { WriteInfo " -> PyPI is reachable." }
# --- Python Discovery ---
$pythonExe = $null
$validPythons = @{} # Hashtable to store path -> version string
$pythonCandidates = @()
Write-Host "`nSearching for Python installations..."
# 1. Check provided path argument
if (-not ([string]::IsNullOrWhiteSpace($PythonPath))) {
$potentialPath = Resolve-Path -Path $PythonPath -ErrorAction SilentlyContinue
if ($potentialPath) {
$pythonExe = $potentialPath.Path
Write-Host " -> Using provided Python path: $pythonExe"
} elseif (Get-Command $PythonPath -ErrorAction SilentlyContinue) {
$pythonExe = $PythonPath # Assume it's in PATH
Write-Host " -> Using Python command from PATH: $pythonExe"
} else {
WriteWarning "Provided Python path/command '$PythonPath' not found."
}
}
# 2. If not found yet, search common locations and PATH
if (-not $pythonExe) {
# Get Python executables from PATH
$pathPythons = (Get-Command python, python3 -ErrorAction SilentlyContinue).
Source
# Add common Windows install locations
if ($IsWindows) {
$commonLocations = @(
"$env:LocalAppData\Programs\Python\Python*",
"$env:ProgramFiles\Python*",
"$env:ProgramFiles(x86)\Python*"
)
foreach ($loc in $commonLocations) {
$pathCandidates = Get-ChildItem -Path $loc -Filter $PyExe -Recurse `
-ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName
$pathPythons += $pathCandidates
}
# Add Windows Python launcher
$pyLauncher = (Get-Command py -ErrorAction SilentlyContinue).Source
if ($pyLauncher) {
$pathPythons += $pyLauncher
}
# Add pyenv-win if it exists
if ($env:PYENV_HOME) {
$pyenvPythons = Get-ChildItem -Path (Join-Path $env:PYENV_HOME "version"
) -Filter $PyExe -Recurse -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty FullName
$pathPythons += $pyenvPythons
}
# Collect uv-installed Python binaries
$uvFolder = Join-Path $env:LocalAppData "uv\python"
$uvPythons = Get-ChildItem -Path $uvFolder -Recurse -File -ErrorAction `
SilentlyContinue | Where-Object { $_.FullName -match
"\\bin\\python[0-9\.]*\.exe$" } |
Select-Object -ExpandProperty FullName
$pathPythons += $uvPythons
} else {
# On Linux/MacOS/BSD, check typical install paths
$commonLocations = @(
"/usr/bin",
"/usr/local/bin",
"/opt/homebrew/bin", # MacOS Homebrew (Apple Silicon)
"/opt/local/bin" # MacPorts
)
foreach ($loc in $commonLocations) {
$pathCandidates = Get-ChildItem -Path $loc -Filter "python*" `
-ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName
$pathPythons += $pathCandidates
}
# Add pyenv if it exists (PYENV_ROOT first, fallback to ~/.pyenv)
$pyenv = if ($env:PYENV_ROOT) { $env:PYENV_ROOT } else { "$HOME/.pyenv" }
# Collect pyenv-installed Python binaries
$pyenvPythons = Get-ChildItem -Path (Join-Path $pyenv "versions"
) -Recurse -File -ErrorAction SilentlyContinue | Where-Object {
$_.FullName -match "/bin/python[0-9\.]*$"
} | Select-Object -ExpandProperty FullName
$pathPythons += $pyenvPythons
# Add uv if it exists (LOCAL_SHARE first, fallback to ~/.local/share)
$uv = if ($env:LOCAL_SHARE) {$env:LOCAL_SHARE} else {"$HOME/.local/share"}
$uvFolder = Join-Path $uv "uv/python"
# Collect uv-installed Python binaries
$uvPythons = Get-ChildItem -Path $uvFolder -Recurse -File -ErrorAction `
SilentlyContinue | Where-Object {
$_.FullName -match "/bin/python[0-9\.]*$"
} | Select-Object -ExpandProperty FullName
$pathPythons += $uvPythons
}
# Deduplicate and validate
$pythonCandidates = $pathPythons | Sort-Object -Unique
foreach ($py in $pythonCandidates) {
try {
$versionInfo = & "$py" -V 2>&1
if ($LASTEXITCODE -eq 0 -and $versionInfo) {
# Extract version string (e.g., "Python 3.10.11")
if ($versionInfo.Trim() -match 'Python\s+([\d\.]+)') {
$validPythons[$py] = $matches[1]
}
}
} catch {
# Ignore errors for invalid executables
}
}
# Select the best candidate if multiple found
if ($validPythons.Count -gt 0) {
Write-Host "Found potential Python interpreters:"
# Sort by actual version number, filtering out versions less than 3.10
$sortedPythonPaths = $validPythons.GetEnumerator() |
Where-Object { [version]$_.Value -ge $MinVer } |
Sort-Object { [version]$_.Value } |
Select-Object -ExpandProperty Key
$i = 0
foreach ($pyPath in $sortedPythonPaths) {
Write-Host " [$i] $($validPythons[$pyPath]) ($pyPath)"
$i++
}
# Prefer python3 if available (Unix default)
$defaultPython = $sortedPythonPaths |
Where-Object { $_ -match "python3(\.exe)?$" } |
Sort-Object { [version]$validPythons[$_] } |
Select-Object -Last 1
if (-not $defaultPython) { ... else python.exe (Windows default)
$defaultPython = $sortedPythonPaths |
Where-Object { $_ -match "python\.exe$" } |
Sort-Object { [version]$validPythons[$_] } |
Select-Object -Last 1
}
if (-not $defaultPython) { ... else plain python (Unix fallback)
$defaultPython = $sortedPythonPaths |
Where-Object { $_ -match "python$" } |
Sort-Object { [version]$validPythons[$_] } |
Select-Object -Last 1
}
if (-not $defaultPython) { ... else take highest version found
$defaultPython = $sortedPythonPaths | Select-Object -Last 1
}
$msg = "`nEnter the index number, full path, or command "
$msg += "name for the Python executable`n(default: '"
$selectedPython = Read-Host $msg$defaultPython"' or index 0)`n"
if ([string]::IsNullOrWhiteSpace($selectedPython)) {
$selectedPython = $defaultPython
}
if ($selectedPython -match '^\d+$') {
$index = [int]$selectedPython
if ($index -ge 0 -and $index -lt $sortedPythonPaths.Count) {
$pythonExe = $sortedPythonPaths[$index]
} else {
ThrowError "Invalid Python index selected."
}
# If it starts with drive letter, it's likely a path
} elseif ($selectedPython -match '^[a-zA-Z]:\\') {
$potentialPath = Resolve-Path -Path $selectedPython `
-ErrorAction SilentlyContinue
if ($potentialPath -and (Test-Path $potentialPath.Path)) {
$pythonExe = $potentialPath.Path
} else {
ThrowError "Invalid Python path provided: $selectedPython"
}
} elseif (Get-Command $selectedPython -ErrorAction SilentlyContinue) {
$pythonExe = $selectedPython # Command from PATH
} else {
ThrowError "Invalid Python selection: $selectedPython"
}
} else {
$msg = "No suitable Python interpreter ($($MinPy)+) found.`n"
ThrowError $msg"Please install Python and ensure it's in your PATH."
}
}
# --- Final Python Validation ---
if (-not $pythonExe) {
ThrowError "Python executable is not set."
}
Write-Host "`nUsing Python: $pythonExe"
try {
$pyVersionCheck = & "$pythonExe" -V 2>&1
if ($LASTEXITCODE -ne 0 -or -not $pyVersionCheck) {
ThrowError "Selected Python '$pythonExe' is invalid or returned an error."
}
Write-Host " -> Python Version Check: $($pyVersionCheck.Trim())"
# Extract version number (e.g. "3.10.1")
if ($pyVersionCheck -match 'Python ([\d.]+)') {
$pyVersion = [version]$matches[1]
if ($pyVersion -lt $MinVer) {
WriteWarning "Python version $pyVersion is older than recommended $MinPy."
} else {
WriteSuccess "Python version $pyVersion meets min. requirement ($MinPy)."
}
} else {
WriteWarning "Could not parse Python version from: $pyVersionCheck"
}
} catch {
ThrowError "Failed to execute Python: $($_.Exception.Message)"
}
# --- Virtual Environment Directory ---
if ([string]::IsNullOrWhiteSpace($VenvDir)) {
$VenvDir = $DefaultVenvDir
$msg = "`nEnter the virtual environment directory path "
$msg += "(default: '$VenvDir')`nPress Enter to accept, or type a new path"
$confirm = Read-Host $msg
if (-not [string]::IsNullOrWhiteSpace($confirm)) {$VenvDir = $confirm }
}
# Normalize path (remove trailing slash unless it's the root)
if ($VenvDir -ne "\" -and $VenvDir.EndsWith("\")) {
$VenvDir = $VenvDir.Substring(0, $VenvDir.Length - 1)
}
# Expand ~ to the user's home directory ($HOME)
# and remove any trailing slashes (both '\' and '/')
$VenvDir = $VenvDir.Replace("~", $HOME).TrimEnd('\','/')
# If trimming left the path empty, reset it to user's home folder
# so it can be flagged as invalid later
if ([string]::IsNullOrEmpty($VenvDir)) {
$VenvDir = $HOME
}
# Get the parent directory of the virtual environment path
# This will be used to detect if the chosen folder is directly under $HOME
$parentDir = Split-Path -Path $VenvDir -Parent
# Prevent installing directly into root or the user's home directory
# If the target is root, home, or directly under home, throw an error
if ($VenvDir -eq $HOME -or $parentDir -eq $HOME) {
$msg = "Installation target '$VenvDir' is too close to the root or "
ThrowError $msg"home directory. Choose a subfolder."
}
# If all checks pass, display the normalized virtual environment directory
Write-Host "Virtual environment directory: $VenvDir"
# Check if target exists
if (Test-Path -Path $VenvDir) {
$overwrite = Read-Host "⚠️ Folder '$VenvDir' already exists. Overwrite? (y/N)"
if ($overwrite -notmatch '^[YySsOo]$') {
Write-Host "❌ Aborting..."
exit 1
}
Write-Host " -> Removing existing directory..."
try {
Remove-Item -Path $VenvDir -Recurse -Force
} catch {
$msg = "Failed to remove existing directory '$VenvDir': "
ThrowError $msg"$($_.Exception.Message)"
}
}
# --- Create Virtual Environment ---
try {
# Ensure parent directory exists
$parentVenvDir = Split-Path -Path $VenvDir -Parent
if (-not (Test-Path -Path $parentVenvDir)) {
New-Item -Path $parentVenvDir -ItemType Directory -Force
}
$venvArgs = "-m venv --copies `"$VenvDir`""
$err = "Failed to create virtual environment."
Write-Host "Creating virtual environment using '$pythonExe' in '$VenvDir'..."
Start-Command $pythonExe $venvArgs $err # Create venv
} catch {
$err = "An error occurred during virtual environment creation: "
ThrowError $err"$($_.Exception.Message)"
}
# --- Install Packages ---
$venvScriptsDir = Join-Path $VenvDir @("bin", "Scripts")[$IsWindows]
$pythonExe = Join-Path $venvScriptsDir @("python", "python.exe")[$IsWindows]
$pipArgs = "-m pip install -U pip"
$err = "Failed to upgrade pip."
Write-Host "Upgrading pip in the virtual environment..."
Start-Command $pythonExe $pipArgs $err
$packArgs = "-m pip install thonny thonny-py5mode[extras] pip-review"
$err = "Failed to install packages."
Write-Host "Installing Thonny, thonny-py5mode, and py5..."
Start-Command $pythonExe $packArgs $err
# --- Create Launchers ---
Write-Host "Creating launchers..."
# 1. Create BAT launcher script (run-thonny.bat)
$batPath = Join-Path $VenvDir "run-thonny.bat"
# $logFolder = Join-Path $VenvDir ".thonny"
# $logFile = Join-Path $logFolder "thonny.log"
$pyExeRelative = Join-Path "Scripts" $PyExe # Relative path within venv
$batContent = @"
@echo off
setlocal
REM Get the directory containing this batch script
set SCRIPT_FOLDER=%~dp0
REM Remove trailing backslash if present
if "%SCRIPT_FOLDER:~-1%"=="\" set SCRIPT_FOLDER=%SCRIPT_FOLDER:~0,-1%
set PYTHON_EXE="%SCRIPT_FOLDER%\$pyExeRelative"
set LOG_FOLDER="%SCRIPT_FOLDER%\.thonny"
set LOG_FILE="%LOG_FOLDER%\thonny.log"
REM Create log directory if it doesn't exist
if not exist "%LOG_FOLDER%" mkdir "%LOG_FOLDER%"
echo Launching Thonny from virtual environment...
echo Logging output to %LOG_FILE%
REM Use start /B to run in background w/o a new window, redirect stdout/stderr
start "" /B "%PYTHON_EXE%" -m thonny > "%LOG_FILE%" 2>&1
endlocal
"@
try {
Set-Content -Path $batPath -Value $batContent -Encoding UTF8
# Make executable conceptually (Windows doesn't have chmod for this)
Unblock-File -Path $batPath
Write-Host " -> Created batch launcher: $batPath"
} catch {
$err = "Failed to create batch launcher '$batPath': "
WriteWarning $err"$($_.Exception.Message)"
}
# --- Create PS1 launcher script (run-thonny.ps1) ---
Write-Host "Creating PowerShell launcher..."
$ps1Path = Join-Path $VenvDir "run-thonny.ps1"
$pyExeRelative = Join-Path @("bin", "Scripts")[$IsWindows] $PyExe
$ps1Content = @'
#!/usr/bin/env pwsh
# PowerShell launcher for Thonny
$SCRIPT_FOLDER = Split-Path -Parent $MyInvocation.MyCommand.Definition
if ($IsWindows) {
$PYTHON_EXE = Join-Path $SCRIPT_FOLDER "Scripts\python.exe"
} else {
$PYTHON_EXE = Join-Path $SCRIPT_FOLDER "bin/python"
}
$LOG_FOLDER = Join-Path $SCRIPT_FOLDER ".thonny"
$LOG_FILE = Join-Path $LOG_FOLDER "thonny.log"
$ERR_FILE = Join-Path $LOG_FOLDER "thonny.err"
if (-not (Test-Path $LOG_FOLDER)) {
New-Item -Path $LOG_FOLDER -ItemType Directory -Force | Out-Null
}
Write-Host "Launching Thonny from virtual environment..."
Write-Host "Logging output to $LOG_FILE"
if ($IsWindows) {
& $PYTHON_EXE -m thonny *>$LOG_FILE
} else {
Start-Process $PYTHON_EXE '-m', 'thonny' `
-RedirectStandardOutput $LOG_FILE -RedirectStandardError $ERR_FILE
}
'@
try {
Set-Content -Path $ps1Path -Value $ps1Content -Encoding UTF8
if (-not $IsWindows) { & chmod +x $ps1Path }
Write-Host " -> Created PowerShell launcher: $ps1Path"
} catch {
$err = "Failed to create PowerShell launcher '$ps1Path': "
WriteWarning $err"$($_.Exception.Message)"
}
# 2. Create Desktop Shortcut
$thonnyExePath = Join-Path $venvScriptsDir $ThonnyExe
$desktopPath = Join-Path $HOME "Desktop"
$shortcutName = "Thonny.lnk"
$shortcutPath = Join-Path $desktopPath $shortcutName
if (Test-Path $thonnyExePath) {
Write-Host " -> Creating desktop shortcut '$shortcutPath'..."
try {
# Target: The Thonny executable inside the venv
# Arguments: (Leave empty for now, Thonny handles its own launch)
# Working Directory: Set to the venv Scripts dir so Thonny can find its
# libs easily
New-Shortcut -TargetFile $thonnyExePath -ShortcutFile $shortcutPath `
-WorkingDirectory $venvScriptsDir
Write-Host " -> Shortcut created on Desktop."
# Copy the shortcut into the venv dir as well (like the bash script did)
$venvShortcutPath = Join-Path $VenvDir $shortcutName
Copy-Item -Path $shortcutPath -Destination $venvShortcutPath -Force
Write-Host " -> Copied shortcut to venv directory."
} catch {
$msg = "Failed to create Thonny desktop shortcut: "
WriteWarning $msg"$($_.Exception.Message)"
}
} else {
$msg = "Thonny executable not found at '$thonnyExePath'. "
WriteWarning $msg"Cannot create shortcut."
}
# --- Final Output ---
# List packages (optional)
$pipListArgs = "-m pip list"
$err = "Failed to list installed packages."
Write-Host "`nInstalled Packages:"
Start-Command $pythonExe $pipListArgs $err
# Show package details (optional)
$pipShowArgs = "-m pip show thonny thonny-py5mode py5"
$err = "Failed to get package details."
Write-Host "`nPackage Details:"
Start-Command $pythonExe $pipShowArgs $err
Write-Host "`n=================================================="
WriteSuccess "Setup Complete!"
Write-Host "Thonny IDE and py5 mode are installed in:"
Write-Host " $VenvDir"
Write-Host ""
Write-Host "You can launch Thonny using:"
Write-Host " 1. The '$shortcutName' shortcut on your Desktop."
Write-Host " 2. Running the '$batPath' script from '$VenvDir'."
Write-Host ""
Write-Host "To activate the virtual environment in PowerShell for manual use:"
$activateScriptPath = Join-Path $venvScriptsDir "Activate.ps1"
Write-Host " & '$activateScriptPath'"
Write-Host ""
Write-Host "To activate the virtual environment in Command Prompt (cmd.exe):"
$activateBatPath = Join-Path $venvScriptsDir "activate.bat"
Write-Host " '$activateBatPath'"
Write-Host "=================================================="
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment