delvewheel is a command-line tool for creating self-contained Python wheel packages for Windows that have DLL dependencies that may not be present on the target system. It is functionally similar to auditwheel (for Linux) and delocate (for macOS).
Suppose that you have built a Python wheel for Windows containing an extension module, and the wheel depends on DLLs that are present in the build environment but may not be present on the end user's machine. This tool determines which DLLs a wheel depends on (aside from system libraries) and copies those DLLs into the wheel. This tool also takes extra steps to avoid DLL hell and to ensure that the DLLs are properly loaded at runtime.
Note: Redistributing third-party DLLs may have copyright or licensing implications, so be sure to review and comply with the relevant licenses before including them in your wheel.
delvewheel can be installed using pip.
pip install delvewheelYou can also install from the source code by opening a command-line shell at the repository root and running
pip install .delvewheel can be run using Python 3.9+ on any platform.
delvewheel can repair wheels targeting Python 2.6+ for win32, win_amd64, or win_arm64.
The environment used to run delvewheel does not need to match the target environment of the wheel being repaired. For example, you can run delvewheel using 32-bit Python 3.9 to repair a wheel for 64-bit Python 2.6. You can even run delvewheel with PyPy3.9 on 32-bit x86 Linux to repair a wheel whose target environment is CPython 3.11 on Windows arm64.
delvewheel show WHEEL: show external DLLs that the wheel depends on
delvewheel repair WHEEL: copy external DLL dependencies into the wheel and patch the wheel so that these libraries are loaded at runtime
delvewheel needed EXECUTABLE: list the direct DLL dependencies of a single executable
delvewheel uses the PATH environment variable to search for DLL dependencies. To specify an additional directory to search for DLLs, add the location of the DLL to the PATH environment variable or use the --add-path option.
delvewheel show and delvewheel repair support specifying multiple wheels in a single invocation. They also support using the * wildcard in the wheel filename.
For a summary of additional command-line options, use the -h option (delvewheel -h, delvewheel show -h, delvewheel repair -h, delvewheel needed -h).
The path separator to use in the following options is ';' on Windows and ':' on Unix-like platforms.
delvewheel show
--add-path PATHS: additional path(s) to search for DLLs, path-separator-delimited. These paths are searched in the given order before those in thePATHenvironment variable.--include DLLS: name(s) of additional DLL(s) to vendor into the wheel, path-separator-delimited. We do not automatically search for dependencies of these DLLs unless another included DLL depends on them. If you use this option, it is your responsibility to ensure that the additional DLL is found at load time.--exclude DLLS: name(s) of DLL(s) to specifically exclude from the wheel, path-separator-delimited. Dependencies of these DLLs are also automatically excluded if no other included DLL depends on them. The*wildcard is supported.--ignore-existing: don't search for or vendor in DLLs that are already in the wheel. We still search for and vendor in dependencies of these DLLs if they are not in the wheel and are transitive dependencies of existing extension modules. This flag is meant for simpler integration with other DLL bundling tools/techniques but is not a catch-all. If you use this flag, it is your responsibility to ensure that the DLLs that are already in the wheel are loaded correctly.--analyze-existing: analyze and vendor in dependencies of DLLs that are already in the wheel. If you use this option, it is your responsibility to ensure that these dependencies are found at load time.--analyze-existing-exes: analyze and vendor in dependencies of EXEs that are in the wheel. If you use this option, it is your responsibility to ensure that these dependencies are found at load time.-v: verbosity-v: level 1, some diagnostic information-vv: level 2, include warnings frompefile
--extract-dir DIR: directory to store extracted contents of wheel for debug use (default is a temp directory)
delvewheel repair
--add-path PATHS: additional path(s) to search for DLLs, path-separator-delimited. These paths are searched in the given order before those in thePATHenvironment variable.--include DLLS: name(s) of additional DLL(s) to vendor into the wheel, path-separator-delimited. We do not automatically search for or vendor in dependencies of these DLLs unless another included DLL depends on them. We do not mangle the names of these DLLs or their direct dependencies. If you use this option, it is your responsibility to ensure that the additional DLL is found at load time.--exclude DLLS: name(s) of DLL(s) to specifically exclude from the wheel, path-separator-delimited. Dependencies of these DLLs are also automatically excluded if no other included DLL depends on them. The*wildcard is supported.--ignore-existing: don't search for or vendor in DLLs that are already in the wheel. Don't mangle the names of these DLLs. Don't mangle the names of their direct dependencies unless--with-mangleis specified. We still search for and vendor in dependencies of these DLLs if they are not in the wheel and are transitive dependencies of existing extension modules. This flag is meant for simpler integration with other DLL bundling tools/techniques but is not a catch-all. If you use this flag, it is your responsibility to ensure that the DLLs that are already in the wheel are loaded correctly.--analyze-existing: analyze and vendor in dependencies of DLLs that are already in the wheel. These dependencies are name-mangled by default. If you use this option, it is your responsibility to ensure that these dependencies are found at load time.--analyze-existing-exes: analyze and vendor in dependencies of EXEs that are in the wheel. These dependencies are name-mangled by default. If you use this option, it is your responsibility to ensure that these dependencies are found at load time.-v: verbosity-v: level 1, some diagnostic information-vv: level 2, include warnings frompefile
--extract-dir DIR: directory to store extracted contents of wheel for debug use (default is a temp directory)-w TARGET,--wheel-dir TARGET: directory to write the repaired wheel (default iswheelhouserelative to current working directory)--no-mangle DLLS: name(s) of DLL(s) not to mangle, path-separator-delimited. The*wildcard is supported.--no-mangle-all: don't mangle any DLL names--with-mangle: see--ignore-existing--strip: strip DLLs that contain an overlay when name-mangling. The GNUstriputility must be present inPATH.-L LIB_SDIR,--lib-sdir LIB_SDIR: directory suffix to store vendored DLLs (default.libs). For example, if your wheel is namedmywheel-0.0.1-cp310-cp310-win_amd64.whl, then the vendored DLLs are stored in themywheel.libsdirectory at the root of the wheel by default. If your wheel contains a top-level extension module that is not in any package, then this setting is ignored, and vendored DLLs are instead placed directly intosite-packageswhen the wheel is installed.--namespace-pkg PKGS: namespace package(s), specified in case-sensitive dot notation and delimited by the path separator. Normally, we patch or create__init__.pyin each top-level package to add the vendored DLL location to the DLL search path at runtime. If you have a top-level namespace package that requires__init__.pyto be absent or unmodified, then this technique can cause problems. This option tellsdelvewheelto use an alternate strategy that does not create or modify__init__.pyat the root of the given namespace package(s). For example,--namespace-pkg package1declarespackage1as a namespace package.- On Windows,
--namespace-pkg package1.package2;package3declarespackage1,package1\package2, andpackage3as namespace packages.
--include-symbols: include.pdbsymbol files with the vendored DLLs. To be included, a symbol file must be in the same directory as the DLL and have the same filename before the extension, e.g.example.dllandexample.pdb.--include-imports: include.libimport library files with the vendored DLLs. To be included, an import library file must be in the same directory as the DLL and have the same filename before the extension, e.g.example.dllandexample.lib.--custom-patch: Normally, we inject a patch into__init__.pyin each top-level package to add the vendored DLL location to the DLL search path at runtime. To precisely control where the DLL search path is modified, use this option to instead specify the exact location(s) to place the patch. When this option is enabled, every line in a.pyfile consisting of the unindented comment# delvewheel: patchis replaced with the patch.
Semantic versioning is used.
This section describes in detail how and why delvewheel mangles the vendored DLL filenames by default. It is fairly technical, so feel free to skip it if it's not relevant to you.
Suppose you install two Python extension modules A.pyd and B.pyd into a single Python environment, where the modules come from separate projects. Each module depends on a DLL named C.dll, so each project ships its own C.dll. Because of how the Windows DLL loader works, if A.pyd is loaded before B.pyd, then both modules end up using A.pyd's version of C.dll. Windows does not allow two DLLs with the same name to be loaded in a single process (unless you have a private SxS assembly, but that's a complicated topic that's best avoided in my opinion). This is a problem if B.pyd is not compatible with A.pyd's version of C.dll. Maybe B.pyd requires a newer version of C.dll than A.pyd. Or maybe the two C.dlls are completely unrelated, and the two project authors by chance chose the same DLL name. This situation is known as DLL hell.
To avoid this issue, delvewheel renames the vendored DLLs. For each DLL, delvewheel appends a hash based on the DLL contents and the hashes of all its direct dependencies which also need to be renamed. For example, if the authors of A.pyd and B.pyd both decided to use delvewheel as part of their projects, then A.pyd's version of C.dll could be renamed to C-a55e90393a19a36b45c623ef23fe3f4a.dll, while B.pyd's version of C.dll could be renamed to C-b7f2aeead421653280728b792642e14f.dll. Now that the two DLLs have different names, they can both be loaded into a single Python process. Even if only one of the two projects decided to use delvewheel, then the two DLLs would have different names, and DLL hell would be avoided.
Simply renaming the DLLs is not enough, though because A.pyd is still looking for C.dll. To fix this, delvewheel goes into A.pyd and finds its import directory table, which tells the Windows loader the names of the DLL dependencies. This table contains an entry with a pointer to the string "C.dll", which is embedded somewhere in A.pyd. delvewheel then finds a suitable location in A.pyd to write the string "C-a55e90393a19a36b45c623ef23fe3f4a.dll" and edits the import directory table entry to point to this string. Now, when A.pyd is loaded, it knows to look for C-a55e90393a19a36b45c623ef23fe3f4a.dll.
So far, we have described the simplest possible example where there exists one Python extension module with one DLL dependency. In the real world, DLL dependency relationships are often more complicated, and delvewheel can handle them as well. For example, suppose a project has the following properties.
- There are two extension modules
D.pydandE.pyd. D.pyddepends onF.dllandG.dll.F.dlldepends onG.dllandH.dll.E.pyddepends onI.dll.I.dlldepends onH.dllandJ.dll.
delvewheel would execute the following when name-mangling.
- Compute the hash of
H.dllfrom its file contents. Suppose it is43c80d2389f603a00e22dd9862246dba. - Compute the hash of
J.dllfrom its file contents. Suppose it is9f50744ed67c3a6e5b24b39c08b2b207. - Compute the hash of
I.dllfrom its file contents and the hashes ofH.dllandJ.dll. Suppose it is348818deee8c8bfbc462c6ba9c8e1898. - Compute the hash of
G.dllfrom its file contents. Suppose it is38752d7e43f7175f4f5e7e906bbeaac7. - Compute the hash of
F.dllfrom its file contents and the hashes ofG.dllandH.dll. Suppose it isc070a14b5ebd1ef22dc434b34bcbb0ae. - Edit the import directory table of
D.pydto point toF-c070a14b5ebd1ef22dc434b34bcbb0ae.dllandG-38752d7e43f7175f4f5e7e906bbeaac7.dll. - Edit the import directory table of
E.pydto point toI-348818deee8c8bfbc462c6ba9c8e1898.dll. - Edit the import directory table of
F.dllto point toG-38752d7e43f7175f4f5e7e906bbeaac7.dllandH-43c80d2389f603a00e22dd9862246dba.dll. - Edit the import directory table of
I.dllto point toH-43c80d2389f603a00e22dd9862246dba.dllandJ-9f50744ed67c3a6e5b24b39c08b2b207.dll. - Rename
F.dlltoF-c070a14b5ebd1ef22dc434b34bcbb0ae.dll. - Rename
G.dlltoG-38752d7e43f7175f4f5e7e906bbeaac7.dll. - Rename
H.dlltoH-43c80d2389f603a00e22dd9862246dba.dll. - Rename
I.dlltoI-348818deee8c8bfbc462c6ba9c8e1898.dll. - Rename
J.dlltoJ-9f50744ed67c3a6e5b24b39c08b2b207.dll.
delvewheel reads DLL file headers to determine which libraries a wheel depends on. DLLs that are loaded at runtime using ctypes/cffi (from Python), LoadLibrary (from C/C++/Rust), or libloading (from Rust) will be missed. Support for runtime-loaded DLLs is limited, and the following options are available.
- Specify additional DLLs to vendor into the wheel using the
--includeoption. - Include the runtime-loaded DLL into the wheel yourself, and use the
--analyze-existingoption.
If you use any of these options, it is your responsibility to ensure that the runtime-loaded DLLs are found at load time.
Wheels may not work on systems older than Windows 7 SP1 because delvewheel excludes system libraries present in Windows 7 SP1+. To create a wheel for an older Windows system that requires an extra system DLL, use the --include flag.
delvewheel is unable to name-mangle a DLL when both of these conditions hold for a dependent of this DLL.
- The dependent contains an overlay (extra data at the end of the binary).
- The dependent contains insufficient internal padding to fit the mangled names.
If this occurs, an exception will be raised with instructions. You have two options.
- Remove the overlay (recommended): The overlay often consists of debug symbols that can be safely stripped via the GNU
striputility. To remove the overlay, executestrip -s EXAMPLE.dllor use the--stripflag withdelvewheel repair. - Skip mangling: To keep the overlay and skip name mangling, use the
--no-mangleor--no-mangle-allflag. This may be necessary if the overlay must be present for the DLL to function or ifstripis unable to remove it.
Any DLL containing an Authenticode signature will have its signature cleared if its dependencies are name-mangled or if it was built with a non-0 value for the /DEPENDENTLOADFLAG linker flag.
delvewheel cannot repair a wheel that includes multiple extension modules targeting different CPU architectures. For example, a wheel containing one extension module targeting win32 and another targeting win_amd64 is not allowed. You should create a separate wheel for each CPU architecture and repair each individually.
If your project has a delay-load DLL dependency, you may need to use a custom delay-load import hook when building the DLL that has the delay-load dependency. This ensures that the directory containing the vendored DLLs is included in the DLL search path when delay-loading. For convenience, we provide a suitable hook for Microsoft Visual C/C++ at delayload/delayhook.c. Add the file to your C/C++ project when building your DLL.
An __init__.py file in a top-level package or a .py file at the root of a namespace package must be parsable by the version of Python that runs delvewheel. For instance, you cannot run delvewheel using Python 3.9 to repair a wheel containing a top-level package with an __init__.py file that uses syntax features introduced in Python 3.10. Aside from this rule, there are no other requirements regarding the relationship between the version of Python that runs delvewheel and the version(s) of Python that the wheel supports.