1269 lines
48 KiB
Plaintext
1269 lines
48 KiB
Plaintext
"""Routines to copy / relink library dependencies in trees and wheels."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import functools
|
|
import logging
|
|
import os
|
|
import re
|
|
import shutil
|
|
import stat
|
|
import warnings
|
|
from os.path import abspath, basename, dirname, exists, realpath, relpath
|
|
from os.path import join as pjoin
|
|
from pathlib import Path
|
|
from subprocess import PIPE, Popen
|
|
from typing import (
|
|
Callable,
|
|
Dict,
|
|
FrozenSet,
|
|
Iterable,
|
|
Iterator,
|
|
List,
|
|
Mapping,
|
|
Optional,
|
|
Set,
|
|
Text,
|
|
Tuple,
|
|
Union,
|
|
)
|
|
|
|
from macholib.mach_o import ( # type: ignore[import-untyped]
|
|
CPU_TYPE_NAMES,
|
|
LC_BUILD_VERSION,
|
|
LC_VERSION_MIN_MACOSX,
|
|
)
|
|
from macholib.MachO import MachO # type: ignore[import-untyped]
|
|
from packaging.utils import parse_wheel_filename
|
|
from packaging.version import Version
|
|
from typing_extensions import Final
|
|
|
|
from .libsana import (
|
|
DelocationError,
|
|
_allow_all,
|
|
get_rp_stripper,
|
|
stripped_lib_dict,
|
|
tree_libs,
|
|
tree_libs_from_directory,
|
|
)
|
|
from .pkginfo import read_pkg_info, write_pkg_info
|
|
from .tmpdirs import TemporaryDirectory
|
|
from .tools import (
|
|
_is_macho_file,
|
|
_remove_absolute_rpaths,
|
|
dir2zip,
|
|
find_package_dirs,
|
|
get_archs,
|
|
set_install_id,
|
|
set_install_name,
|
|
validate_signature,
|
|
zip2dir,
|
|
)
|
|
from .wheeltools import InWheel, rewrite_record
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Prefix for install_name_id of copied libraries
|
|
DLC_PREFIX = "/DLC/"
|
|
|
|
_PLATFORM_REGEXP = re.compile(r"macosx_(\d+)_(\d+)_(\w+)")
|
|
|
|
|
|
def delocate_tree_libs(
|
|
lib_dict: Mapping[Text, Mapping[Text, Text]],
|
|
lib_path: Text,
|
|
root_path: Text,
|
|
*,
|
|
sanitize_rpaths: bool = False,
|
|
) -> Dict[Text, Dict[Text, Text]]:
|
|
"""Move needed libraries in `lib_dict` into `lib_path`.
|
|
|
|
`lib_dict` has keys naming libraries required by the files in the
|
|
corresponding value. Call the keys, "required libs". Call the values
|
|
"requiring objects".
|
|
|
|
Copy all the required libs to `lib_path`. Fix up the rpaths and install
|
|
names in the requiring objects to point to these new copies.
|
|
|
|
Exception: required libs within the directory tree pointed to by
|
|
`root_path` stay where they are, but we modify requiring objects to use
|
|
relative paths to these libraries.
|
|
|
|
Parameters
|
|
----------
|
|
lib_dict : dict
|
|
Dictionary with (key, value) pairs of (``depended_lib_path``,
|
|
``dependings_dict``) (see :func:`libsana.tree_libs`)
|
|
lib_path : str
|
|
Path in which to store copies of libs referred to in keys of
|
|
`lib_dict`. Assumed to exist
|
|
root_path : str
|
|
Root directory of tree analyzed in `lib_dict`. Any required
|
|
library within the subtrees of `root_path` does not get copied, but
|
|
libraries linking to it have links adjusted to use relative path to
|
|
this library.
|
|
sanitize_rpaths : bool, default=False, keyword-only
|
|
If True, absolute paths in rpaths of binaries are removed.
|
|
|
|
Returns
|
|
-------
|
|
copied_libs : dict
|
|
Filtered `lib_dict` dict containing only the (key, value) pairs from
|
|
`lib_dict` where the keys are the libraries copied to `lib_path``.
|
|
|
|
Raises
|
|
------
|
|
DelocationError
|
|
When a malformed `lib_dict` has unresolved paths, missing files, etc.
|
|
When two dependencies would've been be copied to the same destination.
|
|
"""
|
|
# Test for errors first to avoid getting half-way through changing the tree
|
|
libraries_to_copy, libraries_to_delocate = _analyze_tree_libs(
|
|
lib_dict, root_path
|
|
)
|
|
# Copy libraries and update lib_dict.
|
|
lib_dict, copied_libraries = _copy_required_libs(
|
|
lib_dict, lib_path, root_path, libraries_to_copy
|
|
)
|
|
# Update the install names of local and copied libaries.
|
|
_update_install_names(
|
|
lib_dict, root_path, libraries_to_delocate | copied_libraries
|
|
)
|
|
# Remove absolute rpaths
|
|
if sanitize_rpaths:
|
|
_sanitize_rpaths(lib_dict, libraries_to_delocate | copied_libraries)
|
|
|
|
return libraries_to_copy
|
|
|
|
|
|
def _sanitize_rpaths(
|
|
lib_dict: Mapping[Text, Mapping[Text, Text]],
|
|
files_to_delocate: Iterable[Text],
|
|
) -> None:
|
|
"""Sanitize the rpaths of libraries."""
|
|
for required in files_to_delocate:
|
|
# Set relative path for local library
|
|
for requiring, orig_install_name in lib_dict[required].items():
|
|
_remove_absolute_rpaths(requiring)
|
|
|
|
|
|
def _analyze_tree_libs(
|
|
lib_dict: Mapping[Text, Mapping[Text, Text]],
|
|
root_path: Text,
|
|
) -> Tuple[Dict[Text, Dict[Text, Text]], Set[Text]]:
|
|
"""Verify then return which library files to copy and delocate.
|
|
|
|
Returns
|
|
-------
|
|
needs_copying : dict
|
|
The libraries outside of `root_path`.
|
|
This is in the `lib_dict` format for use by `delocate_tree_libs`.
|
|
needs_delocating : set of str
|
|
The libraries inside of `root_path` which need to be delocated.
|
|
"""
|
|
needs_delocating = set() # Libraries which need install names updated.
|
|
needs_copying = {} # A report of which libraries were copied.
|
|
copied_basenames = set()
|
|
rp_root_path = realpath(root_path)
|
|
for required, requirings in lib_dict.items():
|
|
if required.startswith("@"):
|
|
# @rpath, etc, at this point should never happen.
|
|
raise DelocationError("%s was expected to be resolved." % required)
|
|
r_ed_base = basename(required)
|
|
if relpath(required, rp_root_path).startswith(".."):
|
|
# Not local, plan to copy
|
|
if r_ed_base in copied_basenames:
|
|
raise DelocationError(
|
|
"Already planning to copy library with same basename as: "
|
|
+ r_ed_base
|
|
)
|
|
if not exists(required):
|
|
raise DelocationError(
|
|
'library "{0}" does not exist'.format(required)
|
|
)
|
|
# Copy requirings to preserve it since it will be modified later.
|
|
needs_copying[required] = dict(requirings)
|
|
copied_basenames.add(r_ed_base)
|
|
else: # Is local, plan to set relative loader_path
|
|
needs_delocating.add(required)
|
|
return needs_copying, needs_delocating
|
|
|
|
|
|
def _copy_required_libs(
|
|
lib_dict: Mapping[Text, Mapping[Text, Text]],
|
|
lib_path: Text,
|
|
root_path: Text,
|
|
libraries_to_copy: Iterable[Text],
|
|
) -> Tuple[Dict[Text, Dict[Text, Text]], Set[Text]]:
|
|
"""Copy libraries outside of root_path to lib_path.
|
|
|
|
Returns
|
|
-------
|
|
updated_lib_dict : dict
|
|
A copy of `lib_dict` modified so that dependencies now point to
|
|
the copied library destinations.
|
|
needs_delocating : set of str
|
|
A set of the destination files, these need to be delocated.
|
|
"""
|
|
# Create a copy of lib_dict for this script to modify and return.
|
|
out_lib_dict = _copy_lib_dict(lib_dict)
|
|
del lib_dict
|
|
needs_delocating = set() # Set[Text]
|
|
for old_path in libraries_to_copy:
|
|
new_path = realpath(pjoin(lib_path, basename(old_path)))
|
|
logger.info(
|
|
"Copying library %s to %s", old_path, relpath(new_path, root_path)
|
|
)
|
|
shutil.copy(old_path, new_path)
|
|
# Make copied file writeable if necessary.
|
|
statinfo = os.stat(new_path)
|
|
if not statinfo.st_mode & stat.S_IWRITE:
|
|
os.chmod(new_path, statinfo.st_mode | stat.S_IWRITE)
|
|
|
|
# Delocate this file now that it is stored locally.
|
|
needs_delocating.add(new_path)
|
|
# Update out_lib_dict with the new file paths.
|
|
out_lib_dict[new_path] = out_lib_dict[old_path]
|
|
del out_lib_dict[old_path]
|
|
for required in out_lib_dict:
|
|
if old_path not in out_lib_dict[required]:
|
|
continue
|
|
out_lib_dict[required][new_path] = out_lib_dict[required][old_path]
|
|
del out_lib_dict[required][old_path]
|
|
return out_lib_dict, needs_delocating
|
|
|
|
|
|
def _update_install_names(
|
|
lib_dict: Mapping[Text, Mapping[Text, Text]],
|
|
root_path: Text,
|
|
files_to_delocate: Iterable[Text],
|
|
) -> None:
|
|
"""Update the install names of libraries."""
|
|
for required in files_to_delocate:
|
|
# Set relative path for local library
|
|
for requiring, orig_install_name in lib_dict[required].items():
|
|
req_rel = relpath(required, dirname(requiring))
|
|
new_install_name = "@loader_path/" + req_rel
|
|
if orig_install_name == new_install_name:
|
|
logger.info(
|
|
"NOT modifying install name in %s from %s, as the new name"
|
|
" would be the same.",
|
|
relpath(requiring, root_path),
|
|
orig_install_name,
|
|
)
|
|
else:
|
|
logger.info(
|
|
"Modifying install name in %s from %s to %s",
|
|
relpath(requiring, root_path),
|
|
orig_install_name,
|
|
new_install_name,
|
|
)
|
|
set_install_name(requiring, orig_install_name, new_install_name)
|
|
|
|
|
|
def copy_recurse(
|
|
lib_path: Text,
|
|
copy_filt_func: Optional[Callable[[Text], bool]] = None,
|
|
copied_libs: Optional[Dict[Text, Dict[Text, Text]]] = None,
|
|
) -> Dict[Text, Dict[Text, Text]]:
|
|
"""Analyze `lib_path` for library dependencies and copy libraries.
|
|
|
|
`lib_path` is a directory containing libraries. The libraries might
|
|
themselves have dependencies. This function analyzes the dependencies and
|
|
copies library dependencies that match the filter `copy_filt_func`. It also
|
|
adjusts the depending libraries to use the copy. It keeps iterating over
|
|
`lib_path` until all matching dependencies (of dependencies of dependencies
|
|
...) have been copied.
|
|
|
|
Parameters
|
|
----------
|
|
lib_path : str
|
|
Directory containing libraries
|
|
copy_filt_func : None or callable, optional
|
|
If None, copy any library that found libraries depend on. If callable,
|
|
called on each depended library name; copy where
|
|
``copy_filt_func(libname)`` is True, don't copy otherwise
|
|
copied_libs : dict
|
|
Dict with (key, value) pairs of (``copied_lib_path``,
|
|
``dependings_dict``) where ``copied_lib_path`` is the canonical path of
|
|
a library that has been copied to `lib_path`, and ``dependings_dict``
|
|
is a dictionary with (key, value) pairs of (``depending_lib_path``,
|
|
``install_name``). ``depending_lib_path`` is the canonical path of the
|
|
library depending on ``copied_lib_path``, ``install_name`` is the name
|
|
that ``depending_lib_path`` uses to refer to ``copied_lib_path`` (in
|
|
its install names).
|
|
|
|
Returns
|
|
-------
|
|
copied_libs : dict
|
|
Input `copied_libs` dict with any extra libraries and / or dependencies
|
|
added.
|
|
|
|
.. deprecated:: 0.9
|
|
This function is obsolete. :func:`delocate_path` handles recursive
|
|
dependencies while also supporting `@loader_path`.
|
|
"""
|
|
warnings.warn(
|
|
"copy_recurse is obsolete and should no longer be called.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
if copied_libs is None:
|
|
copied_libs = {}
|
|
else:
|
|
copied_libs = dict(copied_libs)
|
|
done = False
|
|
while not done:
|
|
in_len = len(copied_libs)
|
|
_copy_required(lib_path, copy_filt_func, copied_libs)
|
|
done = len(copied_libs) == in_len
|
|
return copied_libs
|
|
|
|
|
|
def _copy_required(
|
|
lib_path: Text,
|
|
copy_filt_func: Optional[Callable[[Text], bool]],
|
|
copied_libs: Dict[Text, Dict[Text, Text]],
|
|
) -> None:
|
|
"""Copy libraries required for files in `lib_path` to `copied_libs`.
|
|
|
|
Augment `copied_libs` dictionary with any newly copied libraries, modifying
|
|
`copied_libs` in-place - see Notes.
|
|
|
|
This is one pass of ``copy_recurse``
|
|
|
|
Parameters
|
|
----------
|
|
lib_path : str
|
|
Directory containing libraries
|
|
copy_filt_func : None or callable, optional
|
|
If None, copy any library that found libraries depend on. If callable,
|
|
called on each library name; copy where ``copy_filt_func(libname)`` is
|
|
True, don't copy otherwise
|
|
copied_libs : dict
|
|
See :func:`copy_recurse` for definition.
|
|
|
|
Notes
|
|
-----
|
|
If we need to copy another library, add that (``depended_lib_path``,
|
|
``dependings_dict``) to `copied_libs`. ``dependings_dict`` has (key,
|
|
value) pairs of (``depending_lib_path``, ``install_name``).
|
|
``depending_lib_path`` will be the original (canonical) library name, not
|
|
the copy in ``lib_path``.
|
|
|
|
Sometimes we copy a library, that further depends on a library we have
|
|
already copied. In this case update ``copied_libs[depended_lib]`` with the
|
|
extra dependency (as well as fixing up the install names for the depending
|
|
library).
|
|
|
|
For example, imagine we've start with a lib path like this::
|
|
|
|
my_lib_path/
|
|
libA.dylib
|
|
libB.dylib
|
|
|
|
Our input `copied_libs` has keys ``/sys/libA.dylib``, ``/sys/libB.lib``
|
|
telling us we previously copied those guys from the ``/sys`` folder.
|
|
|
|
On a first pass, we discover that ``libA.dylib`` depends on
|
|
``/sys/libC.dylib``, so we copy that.
|
|
|
|
On a second pass, we discover now that ``libC.dylib`` also depends on
|
|
``/sys/libB.dylib``. `copied_libs` tells us that we already have a copy of
|
|
``/sys/libB.dylib``, so we fix our copy of `libC.dylib`` to point to
|
|
``my_lib_path/libB.dylib`` and add ``/sys/libC.dylib`` as a
|
|
``dependings_dict`` entry for ``copied_libs['/sys/libB.dylib']``
|
|
|
|
.. deprecated:: 0.9
|
|
This function is obsolete, and is only used by :func:`copy_recurse`.
|
|
"""
|
|
# Paths will be prepended with `lib_path`
|
|
lib_dict = tree_libs(lib_path)
|
|
# Map library paths after copy ('copied') to path before copy ('orig')
|
|
rp_lp = realpath(lib_path)
|
|
copied2orig = dict((pjoin(rp_lp, basename(c)), c) for c in copied_libs)
|
|
for required, requirings in lib_dict.items():
|
|
if copy_filt_func is not None and not copy_filt_func(required):
|
|
continue
|
|
if required.startswith("@"):
|
|
# May have been processed by us, or have some rpath, loader_path of
|
|
# its own. Either way, leave alone
|
|
continue
|
|
# Requiring names may well be the copies in lib_path. Replace the copy
|
|
# names with the original names for entry into `copied_libs`
|
|
procd_requirings = {}
|
|
# Set requiring lib install names to point to local copy
|
|
for requiring, orig_install_name in requirings.items():
|
|
set_install_name(
|
|
requiring,
|
|
orig_install_name,
|
|
"@loader_path/" + basename(required),
|
|
)
|
|
# Make processed version of ``dependings_dict``
|
|
mapped_requiring = copied2orig.get(requiring, requiring)
|
|
procd_requirings[mapped_requiring] = orig_install_name
|
|
if required in copied_libs:
|
|
# Have copied this already, add any new requirings
|
|
copied_libs[required].update(procd_requirings)
|
|
continue
|
|
# Haven't see this one before, add entry to copied_libs
|
|
out_path = pjoin(lib_path, basename(required))
|
|
if exists(out_path):
|
|
raise DelocationError(out_path + " already exists")
|
|
shutil.copy(required, lib_path)
|
|
copied2orig[out_path] = required
|
|
copied_libs[required] = procd_requirings
|
|
|
|
|
|
def _dylibs_only(filename: str) -> bool:
|
|
return filename.endswith(".so") or filename.endswith(".dylib")
|
|
|
|
|
|
def filter_system_libs(libname: str) -> bool:
|
|
"""Return False for system libraries."""
|
|
return not (libname.startswith("/usr/lib") or libname.startswith("/System"))
|
|
|
|
|
|
def _delocate_filter_function(
|
|
path: str,
|
|
*,
|
|
lib_filt_func: Callable[[str], bool],
|
|
copy_filt_func: Callable[[str], bool],
|
|
) -> bool:
|
|
"""Combine the library inspection and copy filters into one function.
|
|
|
|
So that libraries which won't be copied will not be followed.
|
|
"""
|
|
return lib_filt_func(path) and copy_filt_func(path)
|
|
|
|
|
|
def delocate_path(
|
|
tree_path: Text,
|
|
lib_path: Text,
|
|
lib_filt_func: Optional[Union[str, Callable[[Text], bool]]] = None,
|
|
copy_filt_func: Optional[Callable[[Text], bool]] = filter_system_libs,
|
|
executable_path: Optional[Text] = None,
|
|
ignore_missing: bool = False,
|
|
*,
|
|
sanitize_rpaths: bool = False,
|
|
) -> Dict[Text, Dict[Text, Text]]:
|
|
"""Copy required libraries for files in `tree_path` into `lib_path`.
|
|
|
|
Parameters
|
|
----------
|
|
tree_path : str
|
|
Root path of tree to search for required libraries
|
|
lib_path : str
|
|
Directory into which we copy required libraries
|
|
lib_filt_func : None or str or callable, optional
|
|
If None, inspect all files for dependencies on dynamic libraries. If
|
|
callable, accepts filename as argument, returns True if we should
|
|
inspect the file, False otherwise. If str == "dylibs-only" then inspect
|
|
only files with known dynamic library extensions (``.dylib``, ``.so``).
|
|
copy_filt_func : None or callable, optional
|
|
If callable, called on each library name detected as a dependency; copy
|
|
where ``copy_filt_func(libname)`` is True, don't copy otherwise.
|
|
Default is callable rejecting only libraries beginning with
|
|
``/usr/lib`` or ``/System``. None means copy all libraries. This will
|
|
usually end up copying large parts of the system run-time.
|
|
Libraries which won't be copied will not be inspected for dependencies.
|
|
executable_path : None or str, optional
|
|
If not None, an alternative path to use for resolving
|
|
`@executable_path`.
|
|
ignore_missing : bool, default=False
|
|
Continue even if missing dependencies are detected.
|
|
sanitize_rpaths : bool, default=False, keyword-only
|
|
If True, absolute paths in rpaths of binaries are removed.
|
|
|
|
Returns
|
|
-------
|
|
copied_libs : dict
|
|
dict containing the (key, value) pairs of (``copied_lib_path``,
|
|
``dependings_dict``), where ``copied_lib_path`` is a library real path
|
|
that was copied into `lib_sdir` of the wheel packages, and
|
|
``dependings_dict`` is a dictionary with key, value pairs where the key
|
|
is a file in the path depending on ``copied_lib_path``, and the value
|
|
is the ``install_name`` of ``copied_lib_path`` in the depending
|
|
library.
|
|
|
|
Raises
|
|
------
|
|
DelocationError
|
|
When any dependencies can not be located.
|
|
"""
|
|
if lib_filt_func == "dylibs-only":
|
|
lib_filt_func = _dylibs_only
|
|
elif isinstance(lib_filt_func, str):
|
|
raise TypeError('lib_filt_func string can only be "dylibs-only"')
|
|
if lib_filt_func is None:
|
|
lib_filt_func = _allow_all
|
|
if copy_filt_func is None:
|
|
copy_filt_func = _allow_all
|
|
if not exists(lib_path):
|
|
os.makedirs(lib_path)
|
|
# Do not inspect dependencies of libraries that will not be copied.
|
|
filt_func = functools.partial(
|
|
_delocate_filter_function,
|
|
lib_filt_func=lib_filt_func,
|
|
copy_filt_func=copy_filt_func,
|
|
)
|
|
|
|
lib_dict = tree_libs_from_directory(
|
|
tree_path,
|
|
lib_filt_func=filt_func,
|
|
copy_filt_func=filt_func,
|
|
executable_path=executable_path,
|
|
ignore_missing=ignore_missing,
|
|
)
|
|
|
|
return delocate_tree_libs(
|
|
lib_dict, lib_path, tree_path, sanitize_rpaths=sanitize_rpaths
|
|
)
|
|
|
|
|
|
def _copy_lib_dict(
|
|
lib_dict: Mapping[Text, Mapping[Text, Text]],
|
|
) -> Dict[Text, Dict[Text, Text]]:
|
|
"""Return a copy of lib_dict."""
|
|
return { # Convert nested Mapping types into nested Dict types.
|
|
required: dict(requiring) for required, requiring in lib_dict.items()
|
|
}
|
|
|
|
|
|
def _decide_dylib_bundle_directory(
|
|
wheel_dir: str, package_name: str, lib_sdir: str = ".dylibs"
|
|
) -> str:
|
|
"""Return a relative directory which should be used to store dylib files.
|
|
|
|
Parameters
|
|
----------
|
|
wheel_dir : str
|
|
The directory of an unpacked wheel to analyse.
|
|
package_name : str
|
|
The name of the package.
|
|
lib_sdir : str, optional
|
|
Default value for lib sub-directory passed in via
|
|
:func:`delocate_wheel`.
|
|
If wheel has no package directories, used as a suffix.
|
|
|
|
Returns
|
|
-------
|
|
dylibs_dir : str
|
|
A path to within `wheel_dir` where any library files should be put.
|
|
"""
|
|
package_dirs = find_package_dirs(wheel_dir)
|
|
for directory in package_dirs:
|
|
if Path(directory).name == package_name:
|
|
# Prefer using the directory with the same name as the package.
|
|
return pjoin(directory, lib_sdir)
|
|
if package_dirs:
|
|
# Otherwise, store dylib files in the first package alphabetically.
|
|
return pjoin(min(package_dirs), lib_sdir)
|
|
# Otherwise, use an auditwheel-style top-level name.
|
|
return pjoin(wheel_dir, f"{package_name}{lib_sdir}")
|
|
|
|
|
|
def _make_install_name_ids_unique(
|
|
libraries: Iterable[str], install_id_prefix: str
|
|
) -> None:
|
|
"""Replace each library's install name id with a unique id.
|
|
|
|
This is to change install ids to be unique within Python space.
|
|
|
|
Parameters
|
|
----------
|
|
libraries : iterable of str
|
|
The libraries to be modified.
|
|
These files are assumed to be in the same directory.
|
|
install_id_prefix : str
|
|
A unique path to use as a prefix for the install name ids.
|
|
This must be a Unix absolute path.
|
|
|
|
Examples
|
|
--------
|
|
>>> _make_install_name_ids_unique((), "/")
|
|
>>> _make_install_name_ids_unique((), "")
|
|
Traceback (most recent call last):
|
|
...
|
|
ValueError: install_id_prefix should start with '/', got ''
|
|
"""
|
|
if not install_id_prefix.startswith("/"):
|
|
raise ValueError(
|
|
"install_id_prefix should start with '/',"
|
|
f" got {install_id_prefix!r}"
|
|
)
|
|
if not install_id_prefix.endswith("/"):
|
|
install_id_prefix += "/"
|
|
for lib in libraries:
|
|
set_install_id(lib, install_id_prefix + basename(lib))
|
|
validate_signature(lib)
|
|
|
|
|
|
def _get_macos_min_version(dylib_path: Path) -> Iterator[tuple[str, Version]]:
|
|
"""Get the minimum macOS version from a dylib file.
|
|
|
|
Parameters
|
|
----------
|
|
dylib_path : Path
|
|
The path to the dylib file.
|
|
|
|
Yields
|
|
------
|
|
str
|
|
The CPU type.
|
|
Version
|
|
The minimum macOS version.
|
|
"""
|
|
if not _is_macho_file(dylib_path) or dylib_path.suffix == ".a":
|
|
return
|
|
for header in MachO(dylib_path).headers:
|
|
for cmd in header.commands:
|
|
if cmd[0].cmd == LC_BUILD_VERSION:
|
|
version = cmd[1].minos
|
|
elif cmd[0].cmd == LC_VERSION_MIN_MACOSX:
|
|
version = cmd[1].version
|
|
else:
|
|
continue
|
|
yield (
|
|
CPU_TYPE_NAMES.get(header.header.cputype, "unknown"),
|
|
Version(f"{version >> 16 & 0xFF}.{version >> 8 & 0xFF}"),
|
|
)
|
|
break
|
|
|
|
|
|
def _get_archs_and_version_from_wheel_name(
|
|
wheel_name: str,
|
|
) -> dict[str, Version]:
|
|
"""Get the architecture and minimum macOS version from the wheel name.
|
|
|
|
Parameters
|
|
----------
|
|
wheel_name : str
|
|
The name of the wheel.
|
|
|
|
Returns
|
|
-------
|
|
Dict[str, Version]
|
|
A dictionary containing the architecture and minimum macOS version
|
|
for each architecture in the wheel name.
|
|
"""
|
|
platform_tag_set = parse_wheel_filename(wheel_name)[-1]
|
|
platform_requirements = {}
|
|
for platform_tag in platform_tag_set:
|
|
match = _PLATFORM_REGEXP.match(platform_tag.platform)
|
|
if match is None:
|
|
raise ValueError(f"Invalid platform tag: {platform_tag.platform}")
|
|
major, minor, arch = match.groups()
|
|
platform_requirements[arch] = Version(f"{major}.{minor}")
|
|
# If we have a wheel name with arm64 and x86_64 we have to convert that to
|
|
# universal2
|
|
if platform_requirements.keys() == {"arm64", "x86_64"}:
|
|
version = platform_requirements["arm64"]
|
|
if version == Version("11.0"):
|
|
version = platform_requirements["x86_64"]
|
|
platform_requirements = {"universal2": version}
|
|
|
|
return platform_requirements
|
|
|
|
|
|
def _get_incompatible_libs(
|
|
required_version: Optional[Version],
|
|
version_lib_dict: Dict[Version, List[Path]],
|
|
arch: str,
|
|
) -> set[tuple[Path, Version]]:
|
|
"""Find libraries which require a more modern macOS version.
|
|
|
|
Parameters
|
|
----------
|
|
required_version : Version or None
|
|
The expected minimum macOS version.
|
|
If None, return an empty set.
|
|
version_lib_dict : Dict[Version, List[Path]]
|
|
A dictionary containing mapping from minimum macOS version to libraries
|
|
that require that version.
|
|
arch : str
|
|
The architecture of the libraries. For proper handle arm64 case
|
|
|
|
Returns
|
|
-------
|
|
set[tuple[Path, Version]]
|
|
A set of libraries that require a more modern macOS version than the
|
|
provided one.
|
|
"""
|
|
if required_version is None:
|
|
return set()
|
|
if arch == "arm64" and required_version < Version("11.0"):
|
|
# All arm64 libraries require macOS at least 11.0,
|
|
# So even if user provide lower deployment target,
|
|
# for example, by setting environment variable
|
|
# MACOSX_DEPLOYMENT_TARGET=10.15
|
|
# the binaries still will be compatible with 11.0+ only.
|
|
# So there is no need to check for compatible with older macOS versions
|
|
required_version = Version("11.0")
|
|
bad_libraries: set[tuple[Path, Version]] = set()
|
|
for library_version, libraries in version_lib_dict.items():
|
|
if library_version > required_version:
|
|
bad_libraries.update((path, library_version) for path in libraries)
|
|
return bad_libraries
|
|
|
|
|
|
def _unpack_architectures(
|
|
architecture_versions: Mapping[str, Version],
|
|
) -> dict[str, Version]:
|
|
"""Return architecture versions derived from their universal forms.
|
|
|
|
Examples
|
|
--------
|
|
>>> _unpack_architectures({"arm64": Version("11.0")})
|
|
{'arm64': <Version('11.0')>}
|
|
>>> _unpack_architectures({"universal2": Version("10.5")})
|
|
{'x86_64': <Version('10.5')>, 'arm64': <Version('11.0')>}
|
|
>>> _unpack_architectures({"intel": Version("10.5")})
|
|
{'i386': <Version('10.5')>, 'x86_64': <Version('10.5')>}
|
|
>>> _unpack_architectures({})
|
|
{}
|
|
"""
|
|
architecture_versions = {**architecture_versions}
|
|
if "universal2" in architecture_versions:
|
|
architecture_versions["x86_64"] = architecture_versions["universal2"]
|
|
architecture_versions["arm64"] = max(
|
|
architecture_versions["universal2"], Version("11.0")
|
|
)
|
|
del architecture_versions["universal2"]
|
|
if "intel" in architecture_versions:
|
|
architecture_versions["i386"] = architecture_versions["intel"]
|
|
architecture_versions["x86_64"] = architecture_versions["intel"]
|
|
del architecture_versions["intel"]
|
|
return architecture_versions
|
|
|
|
|
|
def _pack_architectures(
|
|
architecture_versions: Mapping[str, Version],
|
|
) -> dict[str, Version]:
|
|
"""Return architecture versions combined into their universal forms.
|
|
|
|
Examples
|
|
--------
|
|
>>> _pack_architectures({"arm64": Version("11.0")})
|
|
{'arm64': <Version('11.0')>}
|
|
>>> _pack_architectures({"i386": Version("10.5"), "x86_64": Version("10.5")})
|
|
{'intel': <Version('10.5')>}
|
|
>>> _pack_architectures({"x86_64": Version("10.5"), "arm64": Version("11.0")})
|
|
{'universal2': <Version('10.5')>}
|
|
>>> _pack_architectures({"x86_64": Version("11.0"), "arm64": Version("12.0")})
|
|
{'x86_64': <Version('11.0')>, 'arm64': <Version('12.0')>}
|
|
>>> _pack_architectures({"i386": Version("11.0"), "x86_64": Version("11.0"), "arm64": Version("11.0")})
|
|
{'i386': <Version('11.0')>, 'universal2': <Version('11.0')>}
|
|
>>> _pack_architectures({})
|
|
{}
|
|
""" # noqa: E501
|
|
architecture_versions = {**architecture_versions}
|
|
if {"x86_64", "arm64"}.issubset(architecture_versions.keys()) and (
|
|
architecture_versions["x86_64"] == architecture_versions["arm64"]
|
|
or architecture_versions["arm64"] == Version("11.0")
|
|
):
|
|
architecture_versions["universal2"] = architecture_versions["x86_64"]
|
|
del architecture_versions["x86_64"]
|
|
del architecture_versions["arm64"]
|
|
if {"i386", "x86_64"}.issubset(
|
|
architecture_versions.keys()
|
|
) and architecture_versions["i386"] == architecture_versions["x86_64"]:
|
|
architecture_versions["intel"] = architecture_versions["i386"]
|
|
del architecture_versions["i386"]
|
|
del architecture_versions["x86_64"]
|
|
return architecture_versions
|
|
|
|
|
|
def _calculate_minimum_wheel_name(
|
|
wheel_name: str,
|
|
wheel_dir: Path,
|
|
require_target_macos_version: Optional[Version],
|
|
) -> tuple[str, set[tuple[Path, Version]]]:
|
|
"""Return a wheel name with an updated platform tag.
|
|
|
|
Based on the architecture of the libraries in the wheel and actual platform
|
|
tag.
|
|
|
|
Parameters
|
|
----------
|
|
wheel_name : str
|
|
The name of the wheel.
|
|
wheel_dir : Path
|
|
The directory of the unpacked wheel.
|
|
require_target_macos_version : Version or None
|
|
The target macOS version that the wheel should be compatible with.
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
The updated wheel name.
|
|
set[tuple[Path, Version]]
|
|
Any libraries requiring a more modern macOS version than
|
|
`require_target_macos_version`.
|
|
"""
|
|
# get platform tag from wheel name using packaging
|
|
if wheel_name.endswith("any.whl"):
|
|
# universal wheel, no need to update the platform tag
|
|
return wheel_name, set()
|
|
wheel_arch_version: Final = _unpack_architectures(
|
|
_get_archs_and_version_from_wheel_name(wheel_name)
|
|
)
|
|
# get the architecture and minimum macOS version from the libraries
|
|
# in the wheel
|
|
all_library_versions: Dict[str, Dict[Version, List[Path]]] = {}
|
|
|
|
for lib in wheel_dir.glob("**/*"):
|
|
for arch, version in _get_macos_min_version(lib):
|
|
all_library_versions.setdefault(arch.lower(), {}).setdefault(
|
|
version, []
|
|
).append(lib)
|
|
logger.debug(
|
|
"Bundled library info: %s arch=%s target=%s",
|
|
lib.name,
|
|
arch,
|
|
version,
|
|
)
|
|
|
|
# Derive architecture requirements from bundled libraries
|
|
arch_version = {
|
|
arch: max(version_libraries.keys())
|
|
for arch, version_libraries in all_library_versions.items()
|
|
}
|
|
|
|
# Compare libraries to target macOS version and track incompatibilities
|
|
incompatible_libs: set[tuple[Path, Version]] = set()
|
|
for arch, version_libraries in all_library_versions.items():
|
|
incompatible_libs.update(
|
|
_get_incompatible_libs(
|
|
require_target_macos_version, version_libraries, arch
|
|
)
|
|
)
|
|
|
|
# Sanity check, wheels tagged with architectures should have at least one
|
|
# bundled library matching that architecture.
|
|
missing_architectures: Final = (
|
|
wheel_arch_version.keys() - arch_version.keys()
|
|
)
|
|
if missing_architectures:
|
|
raise DelocationError(
|
|
"Failed to find any binary with the required architecture: "
|
|
f"""{",".join(missing_architectures)!r}"""
|
|
)
|
|
|
|
# Limit architecture tags to whatever the wheel already claimed to support.
|
|
# Use versions derived from bundled libraries instead of previous wheel tag.
|
|
for arch in arch_version.keys() - wheel_arch_version.keys():
|
|
del arch_version[arch]
|
|
|
|
# Wheel platform tags MUST use the macOS release version, not the literal
|
|
# version provided by macOS. Since macOS 11 the minor version number is not
|
|
# part of the macOS release version and MUST be zero for tagging purposes.
|
|
def get_macos_platform_tag(version: Version, architecture: str) -> str:
|
|
"""Return the macOS platform tag for this version and architecture.
|
|
|
|
`version` will be converted to a release version expected by pip.
|
|
"""
|
|
if require_target_macos_version is not None:
|
|
# Version was specified explicitly with MACOSX_DEPLOYMENT_TARGET.
|
|
version = max(version, require_target_macos_version)
|
|
elif version.major >= 11 and version.minor > 0:
|
|
# This is the range where an automatic version is deceptive.
|
|
logger.warning(
|
|
"Wheel will be tagged as supporting macOS %i (%s),"
|
|
" but will not support macOS versions older than %i.%i\n\t"
|
|
"Configure MACOSX_DEPLOYMENT_TARGET to suppress this warning.",
|
|
version.major,
|
|
architecture,
|
|
version.major,
|
|
version.minor,
|
|
)
|
|
|
|
minor: Final = 0 if version.major >= 11 else version.minor
|
|
return f"macosx_{version.major}_{minor}_{architecture}"
|
|
|
|
platform_tag: Final = ".".join(
|
|
get_macos_platform_tag(version, arch)
|
|
for arch, version in _pack_architectures(arch_version).items()
|
|
)
|
|
prefix: Final = wheel_name.rsplit("-", 1)[0]
|
|
return f"{prefix}-{platform_tag}.whl", incompatible_libs
|
|
|
|
|
|
def _check_and_update_wheel_name(
|
|
wheel_path: Path,
|
|
wheel_dir: Path,
|
|
require_target_macos_version: Optional[Version],
|
|
) -> Path:
|
|
"""Determine the minimum platform tag and update the wheel name if needed.
|
|
|
|
Based on current wheel name and binary files in the wheel.
|
|
|
|
Parameters
|
|
----------
|
|
wheel_path : Path
|
|
The path to the wheel.
|
|
wheel_dir : Path
|
|
The directory of the unpacked wheel.
|
|
require_target_macos_version : Version or None
|
|
The target macOS version that the wheel should be compatible with.
|
|
If provided and the wheel does not satisfy the target MacOS version,
|
|
raise an error.
|
|
"""
|
|
wheel_name = os.path.basename(wheel_path)
|
|
|
|
new_name, problematic_files = _calculate_minimum_wheel_name(
|
|
wheel_name, Path(wheel_dir), require_target_macos_version
|
|
)
|
|
if problematic_files:
|
|
problematic_files_str = "\n".join(
|
|
f"{lib_path} has a minimum target of {lib_macos_version}"
|
|
for lib_path, lib_macos_version in problematic_files
|
|
)
|
|
min_valid_version = max(
|
|
lib_macos_version for _, lib_macos_version in problematic_files
|
|
)
|
|
raise DelocationError(
|
|
"Library dependencies do not satisfy target MacOS"
|
|
f" version {require_target_macos_version}:\n"
|
|
f"{problematic_files_str}"
|
|
"\nSet the environment variable"
|
|
f" 'MACOSX_DEPLOYMENT_TARGET={min_valid_version}'"
|
|
" to update minimum supported macOS for this wheel."
|
|
)
|
|
if new_name != wheel_name:
|
|
wheel_path = wheel_path.parent / new_name
|
|
return wheel_path
|
|
|
|
|
|
def _update_wheelfile(wheel_dir: Path, wheel_name: str) -> None:
|
|
"""
|
|
Update the WHEEL file in the wheel directory with the new platform tag.
|
|
|
|
Parameters
|
|
----------
|
|
wheel_dir : Path
|
|
The directory of the unpacked wheel.
|
|
wheel_name : str
|
|
The name of the wheel.
|
|
Used for determining the new platform tag.
|
|
"""
|
|
platform_tag_set = parse_wheel_filename(wheel_name)[-1]
|
|
(file_path,) = wheel_dir.glob("*.dist-info/WHEEL")
|
|
info = read_pkg_info(file_path)
|
|
del info["Tag"]
|
|
for tag in platform_tag_set:
|
|
info.add_header("Tag", str(tag))
|
|
write_pkg_info(file_path, info)
|
|
|
|
|
|
def delocate_wheel(
|
|
in_wheel: str,
|
|
out_wheel: Optional[str] = None,
|
|
lib_sdir: str = ".dylibs",
|
|
lib_filt_func: Union[None, str, Callable[[str], bool]] = None,
|
|
copy_filt_func: Optional[Callable[[str], bool]] = filter_system_libs,
|
|
require_archs: Union[None, str, Iterable[str]] = None,
|
|
check_verbose: Optional[bool] = None,
|
|
*,
|
|
executable_path: Optional[str] = None,
|
|
ignore_missing: bool = False,
|
|
sanitize_rpaths: bool = False,
|
|
require_target_macos_version: Optional[Version] = None,
|
|
) -> Dict[str, Dict[str, str]]:
|
|
"""Update wheel by copying required libraries to `lib_sdir` in wheel.
|
|
|
|
Create `lib_sdir` in wheel tree only if we are copying one or more
|
|
libraries.
|
|
|
|
If `out_wheel` is None (the default), overwrite the wheel `in_wheel`
|
|
in-place.
|
|
|
|
Parameters
|
|
----------
|
|
in_wheel : str
|
|
Filename of wheel to process
|
|
out_wheel : None or str
|
|
Filename of processed wheel to write. If None, overwrite `in_wheel`
|
|
lib_sdir : str, optional
|
|
Subdirectory name in wheel package directory (or directories) to store
|
|
needed libraries.
|
|
Ignored if the wheel has no package directories, and only contains
|
|
stand-alone modules.
|
|
lib_filt_func : None or str or callable, optional
|
|
If None, inspect all files for dependencies on dynamic libraries. If
|
|
callable, accepts filename as argument, returns True if we should
|
|
inspect the file, False otherwise. If str == "dylibs-only" then inspect
|
|
only files with known dynamic library extensions (``.dylib``, ``.so``).
|
|
copy_filt_func : None or callable, optional
|
|
If callable, called on each library name detected as a dependency; copy
|
|
where ``copy_filt_func(libname)`` is True, don't copy otherwise.
|
|
Default is callable rejecting only libraries beginning with
|
|
``/usr/lib`` or ``/System``. None means copy all libraries. This will
|
|
usually end up copying large parts of the system run-time.
|
|
require_archs : None or str or sequence, optional
|
|
If None, do no checks of architectures in libraries. If sequence,
|
|
sequence of architectures (output from ``lipo -info``) that every
|
|
library in the wheels should have (e.g. ``['x86_64, 'i386']``). An
|
|
empty sequence results in checks that depended libraries have the same
|
|
archs as depending libraries. If string, either "intel" (corresponds
|
|
to sequence ``['x86_64, 'i386']``) or name of required architecture
|
|
(e.g "i386" or "x86_64").
|
|
check_verbose : bool, optional
|
|
This flag is deprecated, and has no effect.
|
|
executable_path : None or str, optional, keyword-only
|
|
An alternative path to use for resolving `@executable_path`.
|
|
ignore_missing : bool, default=False, keyword-only
|
|
Continue even if missing dependencies are detected.
|
|
sanitize_rpaths : bool, default=False, keyword-only
|
|
If True, absolute paths in rpaths of binaries are removed.
|
|
require_target_macos_version : None or Version, optional, keyword-only
|
|
If provided, the minimum macOS version that the wheel should support.
|
|
|
|
Returns
|
|
-------
|
|
copied_libs : dict
|
|
dict containing the (key, value) pairs of (``copied_lib_path``,
|
|
``dependings_dict``), where ``copied_lib_path`` is a library real path
|
|
that was copied into `lib_sdir` of the wheel packages, and
|
|
``dependings_dict`` is a dictionary with key, value pairs where the key
|
|
is a path in the wheel depending on ``copied_lib_path``, and the value
|
|
is the ``install_name`` of ``copied_lib_path`` in the depending
|
|
library. The filenames in the keys are relative to the wheel root path.
|
|
"""
|
|
if check_verbose is not None:
|
|
warnings.warn(
|
|
"The check_verbose flag is deprecated and shouldn't be provided,"
|
|
" all subsequent parameters should be changed over to keywords.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
in_wheel = abspath(in_wheel)
|
|
if out_wheel is None:
|
|
out_wheel = in_wheel
|
|
else:
|
|
out_wheel = abspath(out_wheel)
|
|
in_place = in_wheel == out_wheel
|
|
remove_old = in_place
|
|
with TemporaryDirectory() as tmpdir:
|
|
wheel_dir = realpath(pjoin(tmpdir, "wheel"))
|
|
zip2dir(in_wheel, wheel_dir)
|
|
# Assume the package name from the wheel filename.
|
|
package_name = basename(in_wheel).split("-")[0]
|
|
lib_sdir = _decide_dylib_bundle_directory(
|
|
wheel_dir, package_name, lib_sdir
|
|
)
|
|
lib_path = pjoin(wheel_dir, lib_sdir)
|
|
lib_path_exists_before_delocate = exists(lib_path)
|
|
copied_libs = delocate_path(
|
|
wheel_dir,
|
|
lib_path,
|
|
lib_filt_func,
|
|
copy_filt_func,
|
|
executable_path=executable_path,
|
|
ignore_missing=ignore_missing,
|
|
sanitize_rpaths=sanitize_rpaths,
|
|
)
|
|
if copied_libs and lib_path_exists_before_delocate:
|
|
raise DelocationError(
|
|
f"{lib_path} already exists in wheel but need to copy "
|
|
+ "; ".join(copied_libs)
|
|
)
|
|
if len(os.listdir(lib_path)) == 0:
|
|
shutil.rmtree(lib_path)
|
|
# Check architectures
|
|
if require_archs is not None:
|
|
bads = check_archs(copied_libs, require_archs)
|
|
if bads:
|
|
raise DelocationError(
|
|
"Some missing architectures in wheel"
|
|
f"\n{bads_report(bads, pjoin(tmpdir, 'wheel'))}"
|
|
)
|
|
libraries_in_lib_path = [
|
|
pjoin(lib_path, basename(lib)) for lib in copied_libs
|
|
]
|
|
_make_install_name_ids_unique(
|
|
libraries=libraries_in_lib_path,
|
|
install_id_prefix=DLC_PREFIX + relpath(lib_sdir, wheel_dir),
|
|
)
|
|
rewrite_record(wheel_dir)
|
|
out_wheel_ = Path(out_wheel)
|
|
out_wheel_fixed = _check_and_update_wheel_name(
|
|
out_wheel_, Path(wheel_dir), require_target_macos_version
|
|
)
|
|
if out_wheel_fixed != out_wheel_:
|
|
out_wheel_ = out_wheel_fixed
|
|
in_place = False
|
|
_update_wheelfile(Path(wheel_dir), out_wheel_.name)
|
|
if len(copied_libs) or not in_place:
|
|
if remove_old:
|
|
os.remove(in_wheel)
|
|
logger.info("Deleted:%s", in_wheel)
|
|
dir2zip(wheel_dir, out_wheel_)
|
|
logger.info("Output:%s", out_wheel_)
|
|
return stripped_lib_dict(copied_libs, wheel_dir + os.path.sep)
|
|
|
|
|
|
def patch_wheel(
|
|
in_wheel: Text, patch_fname: Text, out_wheel: Optional[Text] = None
|
|
) -> None:
|
|
"""Apply ``-p1`` style patch in `patch_fname` to contents of `in_wheel`.
|
|
|
|
If `out_wheel` is None (the default), overwrite the wheel `in_wheel`
|
|
in-place.
|
|
|
|
Parameters
|
|
----------
|
|
in_wheel : str
|
|
Filename of wheel to process
|
|
patch_fname : str
|
|
Filename of patch file. Will be applied with ``patch -p1 <
|
|
patch_fname``
|
|
out_wheel : None or str
|
|
Filename of patched wheel to write. If None, overwrite `in_wheel`
|
|
"""
|
|
in_wheel = abspath(in_wheel)
|
|
patch_fname = abspath(patch_fname)
|
|
if out_wheel is None:
|
|
out_wheel = in_wheel
|
|
else:
|
|
out_wheel = abspath(out_wheel)
|
|
if not exists(patch_fname):
|
|
raise ValueError("patch file {0} does not exist".format(patch_fname))
|
|
with InWheel(in_wheel, out_wheel):
|
|
with open(patch_fname, "rb") as fobj:
|
|
patch_proc = Popen(
|
|
["patch", "-p1"], stdin=fobj, stdout=PIPE, stderr=PIPE
|
|
)
|
|
stdout, stderr = patch_proc.communicate()
|
|
if patch_proc.returncode != 0:
|
|
raise RuntimeError(
|
|
"Patch failed with stdout:\n" + stdout.decode("latin1")
|
|
)
|
|
|
|
|
|
_ARCH_LOOKUP = {"intel": ["i386", "x86_64"], "universal2": ["x86_64", "arm64"]}
|
|
|
|
|
|
def check_archs(
|
|
copied_libs: Mapping[Text, Mapping[Text, Text]],
|
|
require_archs: Union[Text, Iterable[Text]] = (),
|
|
stop_fast: bool = False,
|
|
) -> Set[
|
|
Union[Tuple[Text, FrozenSet[Text]], Tuple[Text, Text, FrozenSet[Text]]]
|
|
]:
|
|
"""Check compatibility of archs in `copied_libs` dict.
|
|
|
|
Parameters
|
|
----------
|
|
copied_libs : dict
|
|
dict containing the (key, value) pairs of (``copied_lib_path``,
|
|
``dependings_dict``), where ``copied_lib_path`` is a library real path
|
|
that has been copied during delocation, and ``dependings_dict`` is a
|
|
dictionary with key, value pairs where the key is a path in the target
|
|
being delocated (a wheel or path) depending on ``copied_lib_path``, and
|
|
the value is the ``install_name`` of ``copied_lib_path`` in the
|
|
depending library.
|
|
require_archs : str or sequence, optional
|
|
Architectures we require to be present in all library files in wheel.
|
|
If an empty sequence, just check that depended libraries do have the
|
|
architectures of the depending libraries, with no constraints on what
|
|
these architectures are. If a sequence, then a set of required
|
|
architectures e.g. ``['i386', 'x86_64']`` to specify dual Intel
|
|
architectures. If a string, then a standard architecture name as
|
|
returned by ``lipo -info``, or the string "intel", corresponding to the
|
|
sequence ``['i386', 'x86_64']``, or the string "universal2",
|
|
corresponding to ``['x86_64', 'arm64']``.
|
|
stop_fast : bool, optional
|
|
Whether to give up collecting errors after the first
|
|
|
|
Returns
|
|
-------
|
|
bads : set
|
|
set of length 2 or 3 tuples. A length 2 tuple is of form
|
|
``(depending_lib, missing_archs)`` meaning that an arch in
|
|
`require_archs` was missing from ``depending_lib``. A length 3 tuple
|
|
is of form ``(depended_lib, depending_lib, missing_archs)`` where
|
|
``depended_lib`` is the filename of the library depended on,
|
|
``depending_lib`` is the library depending on ``depending_lib`` and
|
|
``missing_archs`` is a set of missing architecture strings giving
|
|
architectures present in ``depending_lib`` and missing in
|
|
``depended_lib``. An empty set means all architectures were present as
|
|
required.
|
|
"""
|
|
if isinstance(require_archs, str):
|
|
require_archs = _ARCH_LOOKUP.get(require_archs, [require_archs])
|
|
require_archs_set = frozenset(require_archs)
|
|
bads: List[
|
|
Union[Tuple[Text, FrozenSet[Text]], Tuple[Text, Text, FrozenSet[Text]]]
|
|
] = []
|
|
for depended_lib, dep_dict in copied_libs.items():
|
|
depended_archs = get_archs(depended_lib)
|
|
for depending_lib, install_name in dep_dict.items():
|
|
depending_archs = get_archs(depending_lib)
|
|
all_required = depending_archs | require_archs_set
|
|
all_missing = all_required.difference(depended_archs)
|
|
if len(all_missing) == 0:
|
|
continue
|
|
required_missing = require_archs_set.difference(depended_archs)
|
|
if len(required_missing):
|
|
bads.append((depending_lib, required_missing))
|
|
else:
|
|
bads.append((depended_lib, depending_lib, all_missing))
|
|
if stop_fast:
|
|
return set(bads)
|
|
return set(bads)
|
|
|
|
|
|
def bads_report(bads, path_prefix=None):
|
|
"""Return a nice report of bad architectures in `bads`.
|
|
|
|
Parameters
|
|
----------
|
|
bads : set
|
|
set of length 2 or 3 tuples. A length 2 tuple is of form
|
|
``(depending_lib, missing_archs)`` meaning that an arch in
|
|
`require_archs` was missing from ``depending_lib``. A length 3 tuple
|
|
is of form ``(depended_lib, depending_lib, missing_archs)`` where
|
|
``depended_lib`` is the filename of the library depended on,
|
|
``depending_lib`` is the library depending on ``depending_lib`` and
|
|
``missing_archs`` is a set of missing architecture strings giving
|
|
architectures present in ``depending_lib`` and missing in
|
|
``depended_lib``. An empty set means all architectures were present as
|
|
required.
|
|
path_prefix : None or str, optional
|
|
Path prefix to strip from ``depended_lib`` and ``depending_lib``. None
|
|
means do not strip anything.
|
|
|
|
Returns
|
|
-------
|
|
report : str
|
|
A nice report for printing
|
|
"""
|
|
path_processor = (
|
|
(lambda x: x) if path_prefix is None else get_rp_stripper(path_prefix)
|
|
)
|
|
reports = []
|
|
for result in bads:
|
|
if len(result) == 3:
|
|
depended_lib, depending_lib, missing_archs = result
|
|
reports.append(
|
|
"{0} needs {1} {2} missing from {3}".format(
|
|
path_processor(depending_lib),
|
|
"archs" if len(missing_archs) > 1 else "arch",
|
|
", ".join(sorted(missing_archs)),
|
|
path_processor(depended_lib),
|
|
)
|
|
)
|
|
elif len(result) == 2:
|
|
depending_lib, missing_archs = result
|
|
reports.append(
|
|
"Required {0} {1} missing from {2}".format(
|
|
"archs" if len(missing_archs) > 1 else "arch",
|
|
", ".join(sorted(missing_archs)),
|
|
path_processor(depending_lib),
|
|
)
|
|
)
|
|
else:
|
|
raise ValueError("Report tuple should be length 2 or 3")
|
|
return "\n".join(sorted(reports))
|