diff --git a/installers/windows/build.py b/installers/windows/build.py index 0167b9aea..fb91c2538 100644 --- a/installers/windows/build.py +++ b/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_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)