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)
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