Bundle detected DLLs into bin when their packed location is unsearchable - #56
Merged
Conversation
The Windows loader resolves a native extension's imports from the
extension's own directory, ruby.exe's application directory (bin) plus
its ruby_builtin_dlls SxS assembly, and the system directories. PATH is
not consulted on hardened systems, and the AddDllDirectory route gems
take through ruby_installer/runtime does not exist in a packed app.
A detected DLL loaded from a gem's own tree (e.g. FreeTDS, which
tiny_tds ships under ports/), from a devkit's msys64 tree inside the
Ruby prefix, or from outside the prefix entirely was packed only at its
original location, which the loader never searches for imports - the
out-of-prefix case was skipped entirely by a guard that made its
copy_to_bin branch unreachable. Packaged applications then died at
require time with a misleading LoadError ("cannot load such file --
tiny_tds/tiny_tds") on machines where a rich PATH did not mask the gap.
Bundle a copy of every such DLL into bin, next to ruby.exe, mirroring
what the Linux branch achieves with LD_LIBRARY_PATH. DLLs from the
Windows directory always come from the target system and are never
bundled. Companion DLLs found next to native extensions go into bin as
well: an archdir copy only helps extensions in archdir itself, while
the same extension packed at a gem path (openssl and psych are gems
since Ruby 3.x) resolves its imports from bin.
Largo
force-pushed
the
fix/windows-dll-bundle-to-bin
branch
from
August 10, 2026 12:50
95935f7 to
e915887
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The Windows loader resolves a native extension's imports from the extension's own directory, ruby.exe's application directory (
bin) plus itsruby_builtin_dllsSxS assembly, and the system directories. PATH is not consulted on hardened systems, and the AddDllDirectory route gems take throughruby_installer/runtimedoes not exist in a packed app.A detected DLL loaded from anywhere else is packed only at its original location, which the loader never searches when resolving imports:
libsybdb-5.dll), which tiny_tds ships underports/x64-mingw-ucrt/bin/msys64tree inside the Ruby prefix (where e.g.libssl-3-x64.dllgets loaded from when a native gem was compiled on the build machine)next unless dll.subpath?(exec_prefix)guard made the existingcopy_to_binelse-branch unreachable dead codePackaged applications then die at require time with a misleading LoadError (
cannot load such file -- tiny_tds/tiny_tds— the message comes from the gem's fat-binary fallback require, masking the real dependent-DLL failure) on machines where a rich PATH does not mask the gap. CI runners mask it: GitHub's windows runners resolve dependent DLLs via PATH and carry OpenSSL/MSYS2/Perl toolchains on it, so a broken build tests green and only fails in the field (observed with a tiny_tds app running as a Windows service on a production host — the identical exe ran fine on the runner).Fix
Bundle a copy of every detected DLL into
bin, next to ruby.exe, whenever it is not already resolvable from where it gets packed — mirroring what the Linux branch already does with LD_LIBRARY_PATH. Details:ports/dirs at runtime), and additionally land inbinunless they already live underbindirlibssl-3-x64.dllbesideopenssl.soin archdir) also go intobin: an archdir copy only helps extensions in archdir itself, while the same extension packed at a gem path (openssl and psych are gems since Ruby 3.x) resolves its imports frombinTest
test_nonsearched_dll_bundled_to_bin(Windows-only): the fixture dlopens a DLL from aports/bin/directory the way fat binary gems do; the packed exe asserts the DLL was bundled next to ruby.exe.🤖 Generated with Claude Code