Skip to content

Commit

Permalink
Introduce --with-mangle
Browse files Browse the repository at this point in the history
  • Loading branch information
adang1345 committed Jan 14, 2025
1 parent 68773c4 commit fab060a
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 24 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ The path separator to use in the following options is `';'` on Windows and `':'`
- `--add-path`: additional path(s) to search for DLLs, path-separator-delimited. These paths are searched before those in the `PATH` environment variable.
- `--include`: 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`: 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.
- `--ignore-existing`: don't search for or vendor in DLLs that are already in the wheel. Don't mangle the names of these DLLs or their direct dependencies. We still search for and vendor in dependencies of these DLLs if they are not in the wheel. 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.
- `--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-mangle` is specified. We still search for and vendor in dependencies of these DLLs if they are not in the wheel. 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.
- `-v`: verbosity
- `-v`: level 1, some diagnostic information
Expand All @@ -63,6 +63,7 @@ The path separator to use in the following options is `';'` on Windows and `':'`
- `-w`,`--wheel-dir`: directory to write the repaired wheel (default is `wheelhouse` relative to current working directory)
- `--no-mangle`: name(s) of DLL(s) not to mangle, path-separator-delimited
- `--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 GNU `strip` utility must be present in `PATH`.
- `-L`,`--lib-sdir`: directory 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 the `mywheel.libs` directory 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 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,
Expand Down
8 changes: 6 additions & 2 deletions delvewheel/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ def main():
subparser.add_argument('--test', default='', help=argparse.SUPPRESS) # comma-separated testing options, internal use only
parser_repair.add_argument('-w', '--wheel-dir', dest='target', default='wheelhouse', help='directory to write repaired wheel')
parser_repair.add_argument('--no-mangle', action='append', default=[], metavar='DLLS', type=_dll_names, help=f'DLL names(s) not to mangle, {os.pathsep!r}-delimited')
parser_repair.add_argument('--no-mangle-all', action='store_true', help="don't mangle any DLL names")
group = parser_repair.add_mutually_exclusive_group()
group.add_argument('--no-mangle-all', action='store_true', help="don't mangle any DLL names")
group.add_argument('--with-mangle', action='store_true', help='mangle the dependencies of DLLs that are already in the wheel (with --ignore-existing)')
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=_dir_suffix, help='directory suffix to store vendored DLLs (default .libs)')
group = parser_repair.add_mutually_exclusive_group()
Expand Down Expand Up @@ -91,9 +93,11 @@ def main():
if args.command == 'show':
wr.show()
else: # args.command == 'repair'
if args.with_mangle and not args.ignore_existing:
parser_repair.error('--with-mangle requires --ignore-existing')
no_mangles = set(dll_name.lower() for dll_name in os.pathsep.join(args.no_mangle).split(os.pathsep) if dll_name)
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, not args.no_diagnostic and 'SOURCE_DATE_EPOCH' not in os.environ, namespace_pkgs, args.include_symbols, args.include_imports, args.custom_patch)
wr.repair(args.target, no_mangles, args.no_mangle_all, args.with_mangle, args.strip, args.lib_sdir, not args.no_diagnostic and 'SOURCE_DATE_EPOCH' not in os.environ, namespace_pkgs, args.include_symbols, args.include_imports, args.custom_patch)
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
49 changes: 28 additions & 21 deletions delvewheel/_wheel_repair.py
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,7 @@ def repair(
target: str,
no_mangles: set,
no_mangle_all: bool,
with_mangle: bool,
strip: bool,
lib_sdir: str,
log_diagnostics: bool,
Expand All @@ -750,6 +751,9 @@ def repair(
target is the target directory for storing the repaired wheel
no_mangles is a set of lowercase DLL names that will not be mangled
no_mangle_all is True if no DLL name mangling should happen at all
with_mangle is True if the direct dependencies of the DLLs that are
already in the wheel should be name-mangled. Requires
--ignore-existing. Requires that no_mangle_all be False.
strip is True if we should strip DLLs that contain trailing data when
name-mangling
lib_sdir is the suffix for the directory to store the DLLs
Expand Down Expand Up @@ -798,30 +802,30 @@ def repair(

# if --ignore-existing is specified, ignore DLLs that were found inside
# the wheel unless they are specified with --include
dependency_paths_in_wheel, dependency_paths_outside_wheel = self._split_dependency_paths(dependency_paths)
if self._ignore_existing:
dependency_paths_in_wheel, dependency_paths_outside_wheel = self._split_dependency_paths(dependency_paths)
for p in dependency_paths_in_wheel:
name_lower = os.path.basename(p).lower()
no_mangles.add(name_lower)
no_mangles.update(_dll_utils.get_direct_mangleable_needed(p, self._exclude, no_mangles, self._verbose))
if not with_mangle:
no_mangles.update(_dll_utils.get_direct_mangleable_needed(p, self._exclude, no_mangles, self._verbose))
if name_lower not in self._include:
ignored_dll_names.add(name_lower)
dependency_paths = dependency_paths_outside_wheel

# find extra dependencies specified with --include that have not yet
# been found
dependency_names = {os.path.basename(p) for p in dependency_paths} # this is NOT lowercased
dependency_names_lower = {name.lower() for name in dependency_names}
dependency_names_outside_wheel = {os.path.basename(p) for p in dependency_paths_outside_wheel} # this is NOT lowercased
dependency_names_outside_wheel_lower = {name.lower() for name in dependency_names_outside_wheel}
extra_dependency_paths = set()
for dll_name in self._include:
if dll_name in dependency_names_lower:
if dll_name in dependency_names_outside_wheel_lower:
continue
if dll_info := _dll_utils.find_library(dll_name, None, self._arch, include_symbols, include_imports):
extra_dependency_paths.add(dll_info[0])
associated_paths.update(dll_info[1])
else:
raise FileNotFoundError(f'{dll_name} not found')
if not dependency_paths and not extra_dependency_paths:
if not dependency_names_outside_wheel and not extra_dependency_paths:
print('no external dependencies are needed')
os.makedirs(target, exist_ok=True)
shutil.copy2(self._whl_path, target)
Expand All @@ -842,7 +846,7 @@ def repair(
f'not found')

if self._verbose >= 1:
to_copy = set(os.path.basename(p) for p in dependency_paths | extra_dependency_paths)
to_copy = set(os.path.basename(p) for p in dependency_paths_outside_wheel | extra_dependency_paths)
ignored_dll_names -= {name.lower() for name in to_copy}
print(f'External dependencies to copy into the wheel are\n{pp.pformat(to_copy)}')
print(f'External dependencies not to copy into the wheel are\n{pp.pformat(ignored_dll_names)}')
Expand All @@ -859,10 +863,12 @@ def repair(
libs_dir = os.path.join(self._extract_dir, libs_dir_name)
os.makedirs(libs_dir, exist_ok=True)
print(f'copying DLLs into {os.path.relpath(libs_dir, self._extract_dir)}')
for dependency_path in dependency_paths | extra_dependency_paths:
dependency_paths_copied = set()
for dependency_path in dependency_paths_outside_wheel | extra_dependency_paths:
if self._verbose >= 1:
print(f'copying {dependency_path} -> {os.path.join(libs_dir, os.path.basename(dependency_path))}')
shutil.copy2(dependency_path, libs_dir)
dependency_paths_copied.add(os.path.join(libs_dir, os.path.basename(dependency_path)))
for associated_path in associated_paths:
if self._verbose >= 1:
print(f'copying {associated_path} -> {os.path.join(libs_dir, os.path.basename(associated_path))}')
Expand All @@ -874,9 +880,10 @@ def repair(
print('skip mangling DLL names')
else:
print('mangling DLL names')
for lib_name in dependency_names:
# lib_name is NOT lowercased
lib_name_lower = lib_name.lower()
for dependency_path in dependency_paths:
# dependency_path is NOT lowercased
lib_name = os.path.basename(dependency_path)
lib_name_lower = os.path.basename(dependency_path).lower()
if not any(r.fullmatch(lib_name_lower) for r in _dll_list.no_mangle_regexes) and \
lib_name_lower not in no_mangles:
root, ext = os.path.splitext(lib_name)
Expand All @@ -892,8 +899,8 @@ def repair(
print(f'repairing {extension_module_name} -> {extension_module_name}')
needed = _dll_utils.get_direct_mangleable_needed(extension_module_path, self._exclude, no_mangles, self._verbose)
_dll_utils.replace_needed(extension_module_path, needed, name_mangler, strip, self._verbose, self._test)
for lib_name in dependency_names:
lib_path = os.path.join(libs_dir, lib_name)
for dependency_path in dependency_paths_copied | dependency_paths_in_wheel:
lib_name = os.path.basename(dependency_path)
lib_name_lower = lib_name.lower()
if no_mangle_all:
needed = []
Expand All @@ -904,10 +911,10 @@ def repair(
print(f'repairing {lib_name} -> {name_mangler[lib_name_lower]}')
else:
print(f'repairing {lib_name} -> {lib_name}')
needed = _dll_utils.get_direct_mangleable_needed(lib_path, self._exclude, no_mangles, self._verbose)
_dll_utils.replace_needed(lib_path, needed, name_mangler, strip, self._verbose, self._test)
needed = _dll_utils.get_direct_mangleable_needed(dependency_path, self._exclude, no_mangles, self._verbose)
_dll_utils.replace_needed(dependency_path, needed, name_mangler, strip, self._verbose, self._test)
if lib_name_lower in name_mangler:
os.rename(lib_path, os.path.join(libs_dir, name_mangler[lib_name_lower]))
os.rename(dependency_path, os.path.join(libs_dir, name_mangler[lib_name_lower]))

if self._min_supported_python is None or self._min_supported_python < (3, 10):
load_order_filename = f'.load-order-{self._distribution_name}-{self._version}'
Expand Down Expand Up @@ -997,11 +1004,11 @@ def repair(
# delvewheel, the DLLs needed to be listed in a particular order,
# and the old filename has been kept to maintain backward
# compatibility with re-bundling tools such as PyInstaller.
for dependency_name in dependency_names.copy():
for dependency_name in dependency_names_outside_wheel.copy():
# dependency_name is NOT lowercased
if (dependency_name_lower := dependency_name.lower()) in name_mangler:
dependency_names.remove(dependency_name)
dependency_names.add(name_mangler[dependency_name_lower])
dependency_names_outside_wheel.remove(dependency_name)
dependency_names_outside_wheel.add(name_mangler[dependency_name_lower])
# If the wheel contains a top-level extension module, then the
# load-order file will be installed directly into site-packages. To
# avoid conflicts with load-order files from other distributions,
Expand All @@ -1011,7 +1018,7 @@ def repair(
if os.path.exists(load_order_filepath := os.path.join(libs_dir, load_order_filename)):
raise FileExistsError(f'{os.path.relpath(load_order_filepath, self._extract_dir)} already exists')
with open(os.path.join(libs_dir, load_order_filename), 'w', newline='\n') as file:
file.write('\n'.join(dependency_names))
file.write('\n'.join(dependency_names_outside_wheel))
file.write('\n')

# Create .dist-info/DELVEWHEEL file to log repair information. The
Expand Down

0 comments on commit fab060a

Please sign in to comment.