Ver Fonte

fix(windows): bundle vcruntime140_1.dll so greenlet loads on fresh Win10 (#2474)

    A clean Windows 10 install crashed on startup: init_db() -> SQLAlchemy async
    engine -> greenlet failed with "DLL load failed while importing _greenlet:
    The specified module could not be found", so uvicorn never bound :8000 and the
    dashboard refused all connections while the NSSM service still showed running.

    greenlet's _greenlet.pyd is C++ and needs vcruntime140_1.dll, which the
    python.org embeddable distribution does not ship (it includes only
    vcruntime140.dll, enough for the pure-C python313.dll). Machines with the VC++
    2015-2022 redistributable already installed have the DLL in System32, which
    masked the bug in testing.

    Stage vcruntime140_1.dll and msvcp140.dll next to python.exe at build time,
    from a vendored copy or the runner's System32, failing loudly if absent. The
    Inno Setup [Files] step already copies staging\python\* recursively.
maziggy há 2 meses atrás
pai
commit
daecfe6ca0
1 ficheiros alterados com 42 adições e 0 exclusões
  1. 42 0
      installers/windows/build.py

+ 42 - 0
installers/windows/build.py

@@ -61,6 +61,19 @@ FFMPEG_URL = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffm
 # get-pip.py for bootstrapping pip into the embedded distribution
 # get-pip.py for bootstrapping pip into the embedded distribution
 GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py"
 GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py"
 
 
+# C++ runtime DLLs the embeddable distribution does NOT ship. The python.org
+# embeddable zip includes vcruntime140.dll but not vcruntime140_1.dll or
+# msvcp140.dll. python313.dll is pure C and only needs vcruntime140.dll, so
+# python.exe starts fine — but greenlet's _greenlet.pyd is C++ and needs
+# vcruntime140_1.dll (table-based exception handling). On a fresh Windows box
+# that never had the VC++ 2015-2022 redistributable installed, loading greenlet
+# fails with "DLL load failed ... The specified module could not be found",
+# SQLAlchemy's async engine can't start, init_db() raises, and the app never
+# binds its port — the service shows "running" but the dashboard refuses the
+# connection (issue #2474). These runtime DLLs are redistributable, so we ship
+# them app-locally next to python.exe (where vcruntime140.dll already lives).
+VCRUNTIME_DLLS = ("vcruntime140_1.dll", "msvcp140.dll")
+
 
 
 def log(msg: str) -> None:
 def log(msg: str) -> None:
     print(f"[build] {msg}", flush=True)
     print(f"[build] {msg}", flush=True)
@@ -143,6 +156,34 @@ def stage_embedded_python() -> Path:
     return target
     return target
 
 
 
 
+def stage_vcruntime(python_dir: Path) -> None:
+    """Ship the C++ runtime DLLs the embeddable distribution omits.
+
+    Placed next to python.exe so the extension-module loader (which searches the
+    interpreter's own directory) finds them without a redistributable install on
+    the target machine. Prefers vendored copies under installers/windows/vendor/
+    for reproducibility; falls back to the build runner's System32, where the
+    redistributable runtime lives. Fails loudly if neither source has them, so a
+    misconfigured build machine is caught here instead of by end users.
+    """
+    vendor = INSTALLER_DIR / "vendor"
+    system32 = Path(os.environ.get("SYSTEMROOT", r"C:\Windows")) / "System32"
+    for dll in VCRUNTIME_DLLS:
+        dst = python_dir / dll
+        if dst.exists():
+            log(f"{dll} already present in embedded Python")
+            continue
+        src = vendor / dll if (vendor / dll).exists() else system32 / dll
+        if not src.exists():
+            raise RuntimeError(
+                f"required C++ runtime DLL not found: looked in {vendor} and {system32} "
+                f"for {dll}. Install the Microsoft Visual C++ 2015-2022 Redistributable "
+                f"(x64) on the build machine, or vendor {dll} under installers/windows/vendor/."
+            )
+        log(f"staging {dll} from {src}")
+        shutil.copy(src, dst)
+
+
 def install_requirements(python_dir: Path) -> None:
 def install_requirements(python_dir: Path) -> None:
     """Install Bambuddy's requirements.txt into the embedded Python."""
     """Install Bambuddy's requirements.txt into the embedded Python."""
     py = python_dir / "python.exe"
     py = python_dir / "python.exe"
@@ -370,6 +411,7 @@ def main() -> int:
     STAGING.mkdir(parents=True, exist_ok=True)
     STAGING.mkdir(parents=True, exist_ok=True)
 
 
     python_dir = stage_embedded_python()
     python_dir = stage_embedded_python()
+    stage_vcruntime(python_dir)
     if not args.skip_pip:
     if not args.skip_pip:
         install_requirements(python_dir)
         install_requirements(python_dir)