Skip to content

Commit

Permalink
Introduce support for namespace packages
Browse files Browse the repository at this point in the history
  • Loading branch information
adang1345 committed Jul 20, 2023
1 parent b409bda commit 39ebcfe
Show file tree
Hide file tree
Showing 18 changed files with 880 additions and 159 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ The path separator to use in the following options is `';'` on Windows and `':'`
- `--no-mangle-all`: don't mangle any DLL names
- `--strip`: strip DLLs that contain trailing data when name-mangling. The GNU `strip` utility must be present in `PATH`.
- `-L`,`--lib-sdir`: subdirectory suffix to store vendored DLLs (default `.libs`). For example, if your wheel is named `mywheel-0.0.1-cp310-cp310-win_amd64.whl`, then the vendored DLLs are stored in `mywheel.libs` 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 into `site-packages` when the wheel is installed.
- `--namespace-pkg`: namespace packages, specified in case-sensitive dot notation and delimited by the path separator. Normally, we patch or create `__init__.py` in 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__.py` to be absent or unmodified, then this technique can cause problems. This option tells `delvewheel` to use an alternate strategy that does not create or modify `__init__.py` at the root of the given namespace package(s). For example,
- `--namespace-pkg package1` declares `package1` as a namespace package.
- On Windows, `--namespace-pkg package1.package2;package3` declares `package1`, `package1\package2`, and `package3` as namespace packages.

## Limitations

Expand All @@ -71,5 +74,4 @@ The path separator to use in the following options is `';'` on Windows and `':'`
- To avoid DLL hell, we mangle the file names of most DLLs that are vendored into the wheel. This way, a Python process that tries loading a vendored DLL does not end up using a different DLL with the same name. Due to a limitation in how name-mangling is performed, `delvewheel` is unable to name-mangle DLLs whose dependents contain insufficient internal padding to fit the mangled names and contain trailing data at the end of the binary. An exception will be raised if such a DLL is encountered. Commonly, trailing data consist of symbols that can be safely removed using the GNU `strip` utility, although there exist situations where the data must be present for the DLL to function properly. To remove the trailing data, execute `strip -s EXAMPLE.dll` or use the `--strip` flag. To keep the trailing data and skip name mangling, use the `--no-mangle` or `--no-mangle-all` flag.
- Any DLL containing an Authenticode signature will have its signature cleared if its dependencies are name-mangled.
- `delvewheel` cannot repair a wheel that contains extension modules targeting more than one CPU architecture (e.g. both `win32` and `win_amd64`). You should create a separate wheel for each CPU architecture and repair each individually.
- `delvewheel` creates or patches `__init__.py` in each top-level package so that the DLLs are loaded properly during import. This will cause issues if you have a top-level namespace package that requires `__init__.py` to be absent to function properly.
- If your project has a [delay-load DLL dependency](https://learn.microsoft.com/en-us/cpp/build/reference/linker-support-for-delay-loaded-dlls), you must 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](delayload/delayhook.c). Add the file to your C/C++ project when building your DLL.
12 changes: 11 additions & 1 deletion delvewheel/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import argparse
import os
import re
from ._wheel_repair import WheelRepair
from ._version import __version__
from . import _dll_utils
Expand All @@ -21,6 +22,13 @@ def _dll_names(s: str) -> str:
return s


def _namespace_pkgs(s: str) -> str:
for namespace_pkg in filter(None, s.split(os.pathsep)):
if any(c in r'<>:"/\|?*' or ord(c) < 32 for c in namespace_pkg) or not re.fullmatch(r'[^.]+(\.[^.]+)*', namespace_pkg):
raise argparse.ArgumentTypeError(f'Invalid namespace package {namespace_pkg!r}')
return s


def main():
"""Main function"""
# parse arguments
Expand All @@ -46,6 +54,7 @@ def main():
parser_repair.add_argument('--no-mangle-all', action='store_true', help="don't mangle any DLL names")
parser_repair.add_argument('--strip', action='store_true', help='strip DLLs that contain trailing data when name-mangling')
parser_repair.add_argument('-L', '--lib-sdir', default='.libs', type=_subdir_suffix, help='directory suffix in package to store vendored DLLs (default .libs)')
parser_repair.add_argument('--namespace-pkg', default='', metavar='PKGS', type=_namespace_pkgs, help=f'namespace package(s), {os.pathsep!r}-delimited')
parser_repair.add_argument('--no-diagnostic', action='store_true', help=argparse.SUPPRESS) # don't write diagnostic information to DELVEWHEEL metadata file
parser_needed.add_argument('file', help='path to a DLL or PYD file')
parser_needed.add_argument('-v', action='count', default=0, help='verbosity')
Expand All @@ -70,7 +79,8 @@ def main():
wr.show()
else: # args.command == 'repair'
no_mangles = set(dll_name.lower() for dll_name in args.no_mangle.split(os.pathsep) if dll_name)
wr.repair(args.target, no_mangles, args.no_mangle_all, args.strip, args.lib_sdir, args.no_diagnostic)
namespace_pkgs = set(tuple(namespace_pkg.split('.')) for namespace_pkg in args.namespace_pkg.split(os.pathsep) if namespace_pkg)
wr.repair(args.target, no_mangles, args.no_mangle_all, args.strip, args.lib_sdir, args.no_diagnostic, namespace_pkgs)
else: # args.command == 'needed'
for dll_name in sorted(_dll_utils.get_direct_needed(args.file, args.v), key=str.lower):
print(dll_name)
Expand Down
Loading

0 comments on commit 39ebcfe

Please sign in to comment.