Last active
December 2, 2025 12:53
-
-
Save yorgabr/c722f189a2e555f9142095262e2501fa to your computer and use it in GitHub Desktop.
Recreate a clean, non-relocatable-safe Python venv for the current project and fix common issues (broken shebangs, symlinked python, missing pip). Also *optionally* configures CNTLM proxy for pip during this run.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ---------------------------------------------------------------------------- | |
| # ensure_clean_venv_here.sh | |
| # Recreate a clean, non-relocatable-safe Python venv for the current project | |
| # and fix common issues (broken shebangs, symlinked python, missing pip). | |
| # Also optionally configures CNTLM proxy for pip during this run. | |
| # | |
| # Copyright (c) 2025 [Yorga Babuscan](yorgabr@gmail.com) | |
| # | |
| # License: GNU General Public License v3.0 or later (GPL-3.0+) | |
| # This program is free software: you can redistribute it and/or modify | |
| # it under the terms of the GNU GPL as published by the Free Software | |
| # Foundation, either version 3 of the License, or (at your option) any later version. | |
| # | |
| # This program is distributed in the hope that it will be useful, | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| # GNU General Public License for more details. | |
| # | |
| # You should have received a copy of the GNU General Public License | |
| # along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>. | |
| # ---------------------------------------------------------------------------- | |
| # Failsafe shell options for robustness in CI and interactive terminals | |
| set -Eeuo pipefail | |
| IFS=$'\n\t' | |
| # ---- Logging helpers ------------------------------------------------------- | |
| log_info() { printf "[INFO] %s\n" "$*"; } | |
| log_warn() { printf "[WARN] %s\n" "$*"; } | |
| log_error() { printf "[ERROR] %s\n" "$*"; } | |
| # ---- Step 0: Preconditions & environment sanity checks --------------------- | |
| # Ensure bash is running and essential commands exist. | |
| if [ -z "${BASH_VERSION:-}" ]; then | |
| log_error "This script requires bash. Please run: bash ensure_clean_venv_here.sh" | |
| exit 1 | |
| fi | |
| command -v python3 >/dev/null 2>&1 || { | |
| log_error "python3 not found in PATH. Please install Python 3 and try again." | |
| exit 1 | |
| } | |
| # Project root is current directory; venv path is ./.venv | |
| PROJECT_ROOT="$(pwd)" | |
| VENV_DIR="$PROJECT_ROOT/.venv" | |
| PYTHON_BIN="python3" | |
| # ---- Step 1: Detect an existing venv and diagnose if it is broken ---------- | |
| # We check for: | |
| # (a) pip shebang pointing to a non-existent path; and | |
| # (b) venv python being a symlink to /usr/bin/python (undesirable in some setups); and | |
| # (c) missing pip inside venv. | |
| NEEDS_RECREATE=0 | |
| BROKEN_REASONS=() | |
| if [ -d "$VENV_DIR" ]; then | |
| log_info "Detected existing venv at: $VENV_DIR" | |
| if [ -f "$VENV_DIR/bin/pip" ]; then | |
| first_line="$(head -n 1 "$VENV_DIR/bin/pip" || true)" | |
| if [[ "$first_line" =~ ^#! ]]; then | |
| shebang_path="${first_line#!#}" | |
| # Normalize by stripping leading ! and spaces | |
| shebang_path="$(echo "$shebang_path" | sed 's/^!\s*//')" | |
| if [ ! -x "$shebang_path" ]; then | |
| BROKEN_REASONS+=("pip shebang points to non-existent: $shebang_path") | |
| NEEDS_RECREATE=1 | |
| fi | |
| else | |
| BROKEN_REASONS+=("pip script missing valid shebang") | |
| NEEDS_RECREATE=1 | |
| fi | |
| else | |
| BROKEN_REASONS+=("pip script not found in venv") | |
| NEEDS_RECREATE=1 | |
| fi | |
| if [ -L "$VENV_DIR/bin/python" ]; then | |
| target="$(readlink -f "$VENV_DIR/bin/python" || true)" | |
| if [ "$target" = "/usr/bin/python" ] || [ "$target" = "/usr/bin/python3" ]; then | |
| BROKEN_REASONS+=("venv python is a symlink to system python: $target") | |
| NEEDS_RECREATE=1 | |
| fi | |
| fi | |
| else | |
| log_info "No existing venv found; a fresh one will be created." | |
| NEEDS_RECREATE=1 | |
| fi | |
| if [ "$NEEDS_RECREATE" -eq 1 ]; then | |
| if [ ${#BROKEN_REASONS[@]} -gt 0 ]; then | |
| log_warn "Venv appears broken or missing for the following reasons:" | |
| for r in "${BROKEN_REASONS[@]}"; do | |
| log_warn " - $r" | |
| done | |
| fi | |
| else | |
| log_info "Existing venv looks OK. We'll still ensure pip is up-to-date." | |
| fi | |
| # ---- Step 2: Recreate venv using `--copies` to avoid symlinks --------------- | |
| # If venv is broken/missing, we backup the old one (if present) and | |
| # recreate with real binary copies to improve isolation. | |
| if [ "$NEEDS_RECREATE" -eq 1 ]; then | |
| if [ -d "$VENV_DIR" ]; then | |
| ts=$(date +"%Y%m%d-%H%M%S") | |
| backup_dir="$PROJECT_ROOT/.venv.bak-$ts" | |
| log_info "Backing up current venv to: $backup_dir" | |
| mv "$VENV_DIR" "$backup_dir" | |
| fi | |
| log_info "Creating new venv with --copies to avoid symlinks..." | |
| if "$PYTHON_BIN" -m venv --copies "$VENV_DIR"; then | |
| log_info "New venv created successfully: $VENV_DIR" | |
| else | |
| log_warn "--copies failed or unsupported; falling back to standard venv." | |
| "$PYTHON_BIN" -m venv "$VENV_DIR" | |
| log_info "New venv created (standard)." | |
| fi | |
| fi | |
| # ---- Step 3: Ensure pip/setuptools/wheel and restore dependencies ---------- | |
| # We upgrade packaging tools and install project dependencies from | |
| # `requirements.txt` if it is present. | |
| if [ ! -x "$VENV_DIR/bin/python" ]; then | |
| log_error "Venv python not found at $VENV_DIR/bin/python. Aborting." | |
| exit 1 | |
| fi | |
| # ---- Step 4: (Optional) Configure CNTLM proxy for this run ----------------- | |
| # Detect a local CNTLM proxy on 127.0.0.1:3128 and export http/https proxies | |
| # so pip can reach the internet behind NTLM corporate proxies. | |
| setup_cntlm_proxy_for_this_run() { | |
| local host="127.0.0.1" | |
| local port="3128" | |
| local reachable=0 | |
| # Bash TCP test; will succeed if the port is open. | |
| if timeout 1 bash -c "</dev/tcp/$host/$port" 2>/dev/null; then | |
| reachable=1 | |
| fi | |
| if [ "$reachable" -eq 1 ]; then | |
| log_info "CNTLM proxy detected at $host:$port; exporting proxy variables for this run." | |
| export http_proxy="http://$host:$port" | |
| export https_proxy="http://$host:$port" | |
| # Generate a helper script for future activations (manual opt-in): | |
| extras_dir="$VENV_DIR/_extras" | |
| mkdir -p "$extras_dir" | |
| cat > "$extras_dir/proxy_activate.sh" <<'EOS' | |
| # Source this after activating the venv to set CNTLM proxy variables safely. | |
| # Usage: source .venv/_extras/proxy_activate.sh | |
| # It preserves previous proxy values and provides a function to undo. | |
| # Save old values | |
| export _OLD_HTTP_PROXY="$http_proxy" | |
| export _OLD_HTTPS_PROXY="$https_proxy" | |
| # Set CNTLM defaults | |
| export http_proxy="http://127.0.0.1:3128" | |
| export https_proxy="http://127.0.0.1:3128" | |
| undo_proxy() { | |
| # Restore old values (if any) | |
| export http_proxy="${_OLD_HTTP_PROXY}" | |
| export https_proxy="${_OLD_HTTPS_PROXY}" | |
| unset _OLD_HTTP_PROXY _OLD_HTTPS_PROXY | |
| } | |
| EOS | |
| chmod +x "$extras_dir/proxy_activate.sh" | |
| log_info "Created helper: $VENV_DIR/_extras/proxy_activate.sh (manual opt-in)." | |
| else | |
| log_info "CNTLM proxy not detected at 127.0.0.1:3128; continuing without proxy." | |
| fi | |
| } | |
| setup_cntlm_proxy_for_this_run | |
| log_info "Upgrading pip, setuptools and wheel inside the venv..." | |
| "$VENV_DIR/bin/python" -m ensurepip --upgrade || true | |
| "$VENV_DIR/bin/python" -m pip install --upgrade pip setuptools wheel | |
| if [ -f "$PROJECT_ROOT/requirements.txt" ]; then | |
| log_info "Found requirements.txt; installing dependencies..." | |
| "$VENV_DIR/bin/python" -m pip install -r "$PROJECT_ROOT/requirements.txt" | |
| else | |
| log_info "No requirements.txt found; skipping dependency installation." | |
| fi | |
| # Final verification | |
| log_info "Verifying venv executables..." | |
| "$VENV_DIR/bin/python" -V | |
| "$VENV_DIR/bin/pip" -V || { | |
| log_warn "pip invocation failed; you can try: $VENV_DIR/bin/python -m pip -V" | |
| "$VENV_DIR/bin/python" -m pip -V || true | |
| } | |
| cat <<EOF | |
| [INFO] Done. | |
| Next steps: | |
| 1) Activate the venv: source .venv/bin/activate | |
| 2) (Optional) If behind corporate NTLM proxy (CNTLM): | |
| source .venv/_extras/proxy_activate.sh | |
| 3) Check: which python ; which pip ; pip -V ; python -V | |
| 4) If everything is fine, you can delete the old .venv. | |
| EOF | |
| exit 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment