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.
This commit is contained in:
maziggy
2026-07-07 11:05:52 +02:00
+42
View File
@@ -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_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:
print(f"[build] {msg}", flush=True)
@@ -143,6 +156,34 @@ def stage_embedded_python() -> Path:
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:
"""Install Bambuddy's requirements.txt into the embedded Python."""
py = python_dir / "python.exe"
@@ -370,6 +411,7 @@ def main() -> int:
STAGING.mkdir(parents=True, exist_ok=True)
python_dir = stage_embedded_python()
stage_vcruntime(python_dir)
if not args.skip_pip:
install_requirements(python_dir)