From 97d994a079fb030fed6383be75569d70e9b0e2c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 6 Jun 2026 00:45:58 +0200 Subject: [PATCH] fix install v2.15.X --- npm_install.py | 182 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 147 insertions(+), 35 deletions(-) diff --git a/npm_install.py b/npm_install.py index 59a55da..92cb1ab 100644 --- a/npm_install.py +++ b/npm_install.py @@ -1290,6 +1290,66 @@ def _install_certbot_stack(pip_path: Path, certbot_path: Path, env_build: dict): _ensure_certbot_symlink(certbot_path) +def _install_certbot_stack_with_python(python_path: Path, certbot_path: Path, env_build: dict): + """Repair/install Certbot stack using venv python -m pip. + + This is safer on upgraded Debian 11 systems where /opt/certbot/bin/pip + may have a stale shebang, but /opt/certbot/bin/python still works. + """ + log_path = Path("/tmp/npm-certbot-venv.log") + py = str(python_path) + run_logged( + [py, "-m", "pip", "install", "-U", "pip", "setuptools", "wheel"], + log_path, + env=env_build, + ) + run_logged( + [ + py, + "-m", + "pip", + "install", + "-U", + "cryptography", + "cffi", + "certbot", + "tldextract", + ], + log_path, + env=env_build, + ) + + certbot_ver = _detect_certbot_version(certbot_path) + run_logged( + [ + py, + "-m", + "pip", + "install", + "-U", + f"acme=={certbot_ver}", + f"certbot-dns-cloudflare=={certbot_ver}", + f"certbot-dns-rfc2136=={certbot_ver}", + ], + log_path, + env=env_build, + ) + + missing = [] + for pkg in CERTBOT_REQUIRED_PACKAGES: + result = subprocess.run( + [py, "-m", "pip", "show", pkg], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + if result.returncode != 0: + missing.append(pkg) + if missing: + raise RuntimeError(f"Certbot venv incomplete, missing: {', '.join(missing)}") + + _ensure_certbot_symlink(certbot_path) + def _venv_entrypoint_usable(path: Path, args: list[str] | None = None) -> tuple[bool, str]: """Return whether a venv script/binary can be executed. @@ -1330,14 +1390,65 @@ def _venv_entrypoint_usable(path: Path, args: list[str] | None = None) -> tuple[ except Exception as e: return False, f"cannot execute {path}: {e}" -def ensure_certbot_venv_ready(venv_dir: Path = Path("/opt/certbot"), force_rebuild: bool = False): - """Fresh install: build full venv. Update: keep a complete existing venv. +def _venv_python_path(venv_dir: Path) -> Path: + return venv_dir / "bin" / "python" - Rebuild when forced, when packages are missing, or when the existing venv - has stale entrypoints after an OS/Python upgrade. + +def _venv_package_installed_with_python(python_path: Path, pkg: str) -> bool: + try: + result = subprocess.run( + [str(python_path), "-m", "pip", "show", pkg], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + timeout=30, + check=False, + ) + return result.returncode == 0 + except Exception: + return False + + +def _try_repair_existing_certbot_venv(venv_dir: Path, reason: str) -> bool: + """Try to repair an existing venv before deleting it. + + Important for Debian 11: a pip wrapper may point to a removed python3.11, + while /opt/certbot/bin/python still works. In that case rebuild through + pyenv is unnecessary and riskier than repairing with python -m pip. + """ + python_path = _venv_python_path(venv_dir) + certbot_path = venv_dir / "bin" / "certbot" + + python_ok, python_reason = _venv_entrypoint_usable(python_path, ["--version"]) + if not python_ok: + if DEBUG: + print(f" Existing certbot venv python unusable: {python_reason}") + return False + + try: + with step(f"Repairing existing certbot venv ({reason})"): + env_build = os.environ.copy() + env_build["SETUPTOOLS_USE_DISTUTILS"] = "local" + _install_certbot_stack_with_python(python_path, certbot_path, env_build) + + cb_ver = run_out([str(certbot_path), "--version"], check=False).strip() + print(f"✓ Existing certbot venv repaired: {cb_ver}") + return True + except Exception as e: + print(f"⚠ Could not repair existing certbot venv: {e}") + print(" Falling back to full rebuild.") + return False + + +def ensure_certbot_venv_ready(venv_dir: Path = Path("/opt/certbot"), force_rebuild: bool = False): + """Fresh install: build full venv. Update: keep/repair existing venv when possible. + + Debian/Ubuntu upgrades can leave /opt/certbot/bin/pip with a stale shebang. + Do not delete the venv immediately: first try /opt/certbot/bin/python -m pip, + which is safer on Debian 11 where rebuilding Python 3.11 requires pyenv. """ certbot_path = venv_dir / "bin" / "certbot" pip_path = venv_dir / "bin" / "pip" + python_path = _venv_python_path(venv_dir) if force_rebuild and venv_dir.exists(): with step("Removing certbot venv for forced rebuild"): @@ -1347,46 +1458,47 @@ def ensure_certbot_venv_ready(venv_dir: Path = Path("/opt/certbot"), force_rebui rebuild_reason = "forced rebuild" if force_rebuild else "" if not needs_rebuild: - pip_ok, pip_reason = _venv_entrypoint_usable(pip_path, ["--version"]) - certbot_ok, certbot_reason = _venv_entrypoint_usable(certbot_path, ["--version"]) - if not pip_ok or not certbot_ok: + if not venv_dir.exists(): needs_rebuild = True - rebuild_reason = pip_reason if not pip_ok else certbot_reason + rebuild_reason = f"missing: {venv_dir}" + else: + python_ok, python_reason = _venv_entrypoint_usable(python_path, ["--version"]) + certbot_ok, certbot_reason = _venv_entrypoint_usable(certbot_path, ["--version"]) + pip_ok, pip_reason = _venv_entrypoint_usable(pip_path, ["--version"]) - if not needs_rebuild: - for pkg in CERTBOT_REQUIRED_PACKAGES: - try: - result = subprocess.run( - [str(pip_path), "show", pkg], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - check=False, - ) - except FileNotFoundError as e: - needs_rebuild = True - rebuild_reason = f"broken pip wrapper: {e}" - break - except Exception as e: - needs_rebuild = True - rebuild_reason = f"cannot verify certbot venv: {e}" - break + packages_ok = python_ok + missing_pkg = None + if packages_ok: + for pkg in CERTBOT_REQUIRED_PACKAGES: + if not _venv_package_installed_with_python(python_path, pkg): + packages_ok = False + missing_pkg = pkg + break - if result.returncode != 0: - needs_rebuild = True - rebuild_reason = f"missing package: {pkg}" - if DEBUG: - print(f" Certbot venv missing package: {pkg}") - break + if python_ok and certbot_ok and packages_ok: + if not pip_ok: + # Wrapper is stale, but the venv itself works. Repair scripts in place. + _try_repair_existing_certbot_venv(venv_dir, pip_reason) + _ensure_certbot_symlink(certbot_path) + cb_ver = run_out([str(certbot_path), "--version"], check=False).strip() + print(f"✓ Existing certbot venv is complete: {cb_ver}") + run(["chown", "-R", "npm:npm", str(venv_dir)], check=False) + return True + + if python_ok: + reason = certbot_reason if not certbot_ok else (f"missing package: {missing_pkg}" if missing_pkg else pip_reason) + if _try_repair_existing_certbot_venv(venv_dir, reason): + run(["chown", "-R", "npm:npm", str(venv_dir)], check=False) + return True + + needs_rebuild = True + rebuild_reason = python_reason if not python_ok else (certbot_reason if not certbot_ok else (f"missing package: {missing_pkg}" if missing_pkg else pip_reason)) if needs_rebuild: if venv_dir.exists(): with step(f"Removing broken certbot venv ({rebuild_reason})"): shutil.rmtree(venv_dir, ignore_errors=True) setup_certbot_venv(venv_dir) - else: - _ensure_certbot_symlink(certbot_path) - cb_ver = run_out([str(certbot_path), "--version"], check=False).strip() - print(f"✓ Existing certbot venv is complete: {cb_ver}") run(["chown", "-R", "npm:npm", str(venv_dir)], check=False) return True