Skip to content

Instantly share code, notes, and snippets.

@yorgabr
Last active December 2, 2025 12:53
Show Gist options
  • Select an option

  • Save yorgabr/c722f189a2e555f9142095262e2501fa to your computer and use it in GitHub Desktop.

Select an option

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.
#!/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