fix install v2.15.X

This commit is contained in:
Mateusz Gruszczyński
2026-06-06 00:45:58 +02:00
parent 9ececc5843
commit 97d994a079
+147 -35
View File
@@ -1290,6 +1290,66 @@ def _install_certbot_stack(pip_path: Path, certbot_path: Path, env_build: dict):
_ensure_certbot_symlink(certbot_path) _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]: def _venv_entrypoint_usable(path: Path, args: list[str] | None = None) -> tuple[bool, str]:
"""Return whether a venv script/binary can be executed. """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: except Exception as e:
return False, f"cannot execute {path}: {e}" return False, f"cannot execute {path}: {e}"
def ensure_certbot_venv_ready(venv_dir: Path = Path("/opt/certbot"), force_rebuild: bool = False): def _venv_python_path(venv_dir: Path) -> Path:
"""Fresh install: build full venv. Update: keep a complete existing venv. 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" certbot_path = venv_dir / "bin" / "certbot"
pip_path = venv_dir / "bin" / "pip" pip_path = venv_dir / "bin" / "pip"
python_path = _venv_python_path(venv_dir)
if force_rebuild and venv_dir.exists(): if force_rebuild and venv_dir.exists():
with step("Removing certbot venv for forced rebuild"): 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 "" rebuild_reason = "forced rebuild" if force_rebuild else ""
if not needs_rebuild: if not needs_rebuild:
pip_ok, pip_reason = _venv_entrypoint_usable(pip_path, ["--version"]) if not venv_dir.exists():
certbot_ok, certbot_reason = _venv_entrypoint_usable(certbot_path, ["--version"])
if not pip_ok or not certbot_ok:
needs_rebuild = True 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: packages_ok = python_ok
for pkg in CERTBOT_REQUIRED_PACKAGES: missing_pkg = None
try: if packages_ok:
result = subprocess.run( for pkg in CERTBOT_REQUIRED_PACKAGES:
[str(pip_path), "show", pkg], if not _venv_package_installed_with_python(python_path, pkg):
stdout=subprocess.DEVNULL, packages_ok = False
stderr=subprocess.DEVNULL, missing_pkg = pkg
check=False, break
)
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
if result.returncode != 0: if python_ok and certbot_ok and packages_ok:
needs_rebuild = True if not pip_ok:
rebuild_reason = f"missing package: {pkg}" # Wrapper is stale, but the venv itself works. Repair scripts in place.
if DEBUG: _try_repair_existing_certbot_venv(venv_dir, pip_reason)
print(f" Certbot venv missing package: {pkg}") _ensure_certbot_symlink(certbot_path)
break 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 needs_rebuild:
if venv_dir.exists(): if venv_dir.exists():
with step(f"Removing broken certbot venv ({rebuild_reason})"): with step(f"Removing broken certbot venv ({rebuild_reason})"):
shutil.rmtree(venv_dir, ignore_errors=True) shutil.rmtree(venv_dir, ignore_errors=True)
setup_certbot_venv(venv_dir) 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) run(["chown", "-R", "npm:npm", str(venv_dir)], check=False)
return True return True