class DelocationError(Exception):
    pass 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)) def check_archs(
    copied_libs,  # type: Mapping[Text, Mapping[Text, Text]]
    require_archs=(),  # type:  Union[Text, Iterable[Text]]
    stop_fast=False,  # type: bool
):
    # type: (...) -> Set[Union[Tuple[Text, FrozenSet[Text]], Tuple[Text, Text, FrozenSet[Text]]]]  # noqa: E501
    """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 = (
        []
    )  # type: List[Union[Tuple[Text, FrozenSet[Text]], Tuple[Text, Text, FrozenSet[Text]]]]  # noqa: E501
    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 copy_recurse(
    lib_path,  # type: Text
    copy_filt_func=None,  # type: Optional[Callable[[Text], bool]]
    copied_libs=None,  # type: Optional[Dict[Text, Dict[Text, Text]]]
):
    # type: (...) -> 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 delocate_path(
    tree_path,  # type: Text
    lib_path,  # type: Text
    lib_filt_func=None,  # type: Optional[Union[str, Callable[[Text], bool]]]
    copy_filt_func=filter_system_libs,  # type: Optional[Callable[[Text], bool]]
    executable_path=None,  # type: Optional[Text]
    ignore_missing=False,  # type: bool
):
    # type: (...) -> 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.

    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) def delocate_tree_libs(
    lib_dict,  # type: Mapping[Text, Mapping[Text, Text]]
    lib_path,  # type: Text
    root_path,  # type: Text
):
    # type: (...) -> 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.

    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
    )
    return libraries_to_copy def filter_system_libs(libname: str) -> bool:
    return not (libname.startswith("/usr/lib") or libname.startswith("/System")) def search_environment_for_lib(lib_path):
    # type: (Text) -> Text
    """Search common environment variables for `lib_path`

    We'll use a single approach here:

        1. Search for the basename of the library on DYLD_LIBRARY_PATH
        2. Search for ``realpath(lib_path)``
        3. Search for the basename of the library on DYLD_FALLBACK_LIBRARY_PATH

    This follows the order that Apple defines for "searching for a
    library that has a directory name in it" as defined in their
    documentation here:

    https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/DynamicLibraries/100-Articles/DynamicLibraryUsageGuidelines.html#//apple_ref/doc/uid/TP40001928-SW10

    See the script "testing_osx_rpath_env_variables.sh" in tests/data
    for a more in-depth explanation. The case where LD_LIBRARY_PATH is
    used is a narrow subset of that, so we'll ignore it here to keep
    things simple.

    Parameters
    ----------
    lib_path : str
        Name of the library to search for

    Returns
    -------
    lib_path : str
        Real path of the first found location, if it can be found, or
        ``realpath(lib_path)`` if it cannot.
    """
    lib_basename = basename(lib_path)
    potential_library_locations = []

    # 1. Search on DYLD_LIBRARY_PATH
    potential_library_locations += _paths_from_var(
        "DYLD_LIBRARY_PATH", lib_basename
    )

    # 2. Search for realpath(lib_path)
    potential_library_locations.append(realpath(lib_path))

    # 3. Search on DYLD_FALLBACK_LIBRARY_PATH
    potential_library_locations += _paths_from_var(
        "DYLD_FALLBACK_LIBRARY_PATH", lib_basename
    )

    for location in potential_library_locations:
        if os.path.exists(location):
            # See GH#133 for why we return the realpath here if it can be found
            return realpath(location)
    return realpath(lib_path) def tree_libs(
    start_path,  # type: Text
    filt_func=None,  # type: Optional[Callable[[Text], bool]]
):
    # type: (...) -> Dict[Text, Dict[Text, Text]]
    """Return analysis of library dependencies within `start_path`

    Parameters
    ----------
    start_path : str
        root path of tree to search for libraries depending on other libraries.
    filt_func : None or callable, optional
        If None, inspect all files for library dependencies. If callable,
        accepts filename as argument, returns True if we should inspect the
        file, False otherwise.
    Returns
    -------
    lib_dict : dict
        dictionary with (key, value) pairs of (``libpath``,
        ``dependings_dict``).

        ``libpath`` is a canonical (``os.path.realpath``) filename of library,
        or library name starting with {'@loader_path'}.


        ``dependings_dict`` is a dict with (key, value) pairs of
        (``depending_libpath``, ``install_name``), where ``dependings_libpath``
        is the canonical (``os.path.realpath``) filename of the library
        depending on ``libpath``, and ``install_name`` is the "install_name" by
        which ``depending_libpath`` refers to ``libpath``.

    Notes
    -----

    See:

    * https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/dyld.1.html  # noqa: E501
    * http://matthew-brett.github.io/pydagogue/mac_runtime_link.html

    .. deprecated:: 0.9
        This function does not support `@loader_path` and only returns the
        direct dependencies of the libraries in `start_path`.

        :func:`tree_libs_from_directory` should be used instead.
    """
    warnings.warn(
        "tree_libs doesn't support @loader_path and has been deprecated.",
        DeprecationWarning,
        stacklevel=2,
    )
    if filt_func is None:
        filt_func = _allow_all
    lib_dict = {}  # type: Dict[Text, Dict[Text, Text]]
    for dirpath, dirnames, basenames in os.walk(start_path):
        for base in basenames:
            depending_path = realpath(pjoin(dirpath, base))
            for dependency_path, install_name in get_dependencies(
                depending_path,
                filt_func=filt_func,
            ):
                if dependency_path is None:
                    # Mimic deprecated behavior.
                    # A lib_dict with unresolved paths is unsuitable for
                    # delocating, this is a missing dependency.
                    dependency_path = realpath(install_name)
                if install_name.startswith("@loader_path/"):
                    # Support for `@loader_path` would break existing callers.
                    logger.debug(
                        "Excluding %s because it has '@loader_path'.",
                        install_name,
                    )
                    continue
                lib_dict.setdefault(dependency_path, {})
                lib_dict[dependency_path][depending_path] = install_name
    return lib_dict def tree_libs_from_directory(
    start_path: str,
    *,
    lib_filt_func: Callable[[str], bool] = _filter_system_libs,
    copy_filt_func: Callable[[str], bool] = lambda path: True,
    executable_path: Optional[str] = None,
    ignore_missing: bool = False,
) -> Dict[Text, Dict[Text, Text]]:
    """Return an analysis of the libraries in the directory of `start_path`.

    Parameters
    ----------
    start_path : iterable of str
        Root path of tree to search for libraries depending on other libraries.
    lib_filt_func : callable, optional, keyword-only
        A callable which accepts filename as argument and returns True if we
        should inspect the file or False otherwise.
        If `filt_func` filters a library it will will not further analyze any
        of that library's dependencies.
        Defaults to inspecting all files except for system libraries.
    copy_filt_func : callable, optional, keyword-only
        Called on each library name detected as a dependency; copy
        where ``copy_filt_func(libname)`` is True, don't copy otherwise.
        Defaults to copying all detected dependencies.
    executable_path : None or str, optional, keyword-only
        If not None, an alternative path to use for resolving
        `@executable_path`.
    ignore_missing : bool, default=False, optional, keyword-only
        Continue even if missing dependencies are detected.

    Returns
    -------
    lib_dict : dict
        dictionary with (key, value) pairs of (``libpath``,
        ``dependings_dict``).

        ``libpath`` is a canonical (``os.path.realpath``) filename of library,
        or library name starting with {'@loader_path'}.


        ``dependings_dict`` is a dict with (key, value) pairs of
        (``depending_libpath``, ``install_name``), where ``dependings_libpath``
        is the canonical (``os.path.realpath``) filename of the library
        depending on ``libpath``, and ``install_name`` is the "install_name" by
        which ``depending_libpath`` refers to ``libpath``.

    Raises
    ------
    DelocationError
        When any dependencies can not be located and ``ignore_missing`` is
        False.
    """
    return _tree_libs_from_libraries(
        walk_directory(
            start_path, lib_filt_func, executable_path=executable_path
        ),
        lib_filt_func=lib_filt_func,
        copy_filt_func=copy_filt_func,
        ignore_missing=ignore_missing,
    ) class InTemporaryDirectory(TemporaryDirectory):
    """Create, return, and change directory to a temporary directory

    Examples
    --------
    >>> import os
    >>> my_cwd = os.getcwd()
    >>> with InTemporaryDirectory() as tmpdir:
    ...     _ = open('test.txt', 'wt').write('some text')
    ...     assert os.path.isfile('test.txt')
    ...     assert os.path.isfile(os.path.join(tmpdir, 'test.txt'))
    >>> os.path.exists(tmpdir)
    False
    >>> os.getcwd() == my_cwd
    True
    """

    def __enter__(self):
        self._pwd = os.getcwd()
        os.chdir(self.name)
        return super(InTemporaryDirectory, self).__enter__()

    def __exit__(self, exc, value, tb):
        os.chdir(self._pwd)
        return super(InTemporaryDirectory, self).__exit__(exc, value, tb) def get_install_names(filename: str) -> Tuple[str, ...]:
    """Return install names from library named in `filename`

    Returns tuple of install names

    tuple will be empty if no install names, or if this is not an object file.

    Parameters
    ----------
    filename : str
        filename of library

    Returns
    -------
    install_names : tuple
        tuple of install names for library `filename`

    Raises
    ------
    NotImplementedError
        If ``filename`` has different install names per-architecture.
    InstallNameError
        On any unexpected output from ``otool``.
    """
    otool = subprocess.run(
        ["otool", "-L", filename],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        universal_newlines=True,
    )
    if not _line0_says_object(otool.stdout or otool.stderr, filename):
        return ()
    install_id = get_install_id(filename)
    names_data = _check_ignore_archs(_parse_otool_install_names(otool.stdout))
    names = [name for name, _, _ in names_data]
    if install_id:  # Remove redundant install id from the install names.
        if names[0] != install_id:
            raise InstallNameError(
                f"Expected {install_id!r} to be first in {names}"
            )
        names = names[1:]
    return tuple(names) @ensure_writable
def set_install_name(
    filename: str, oldname: str, newname: str, ad_hoc_sign: bool = True
) -> None:
    """Set install name `oldname` to `newname` in library filename

    Parameters
    ----------
    filename : str
        filename of library
    oldname : str
        current install name in library
    newname : str
        replacement name for `oldname`
    ad_hoc_sign : {True, False}, optional
        If True, sign library with ad-hoc signature
    """
    names = get_install_names(filename)
    if oldname not in names:
        raise InstallNameError(
            "{0} not in install names for {1}".format(oldname, filename)
        )
    subprocess.run(
        ["install_name_tool", "-change", oldname, newname, filename],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        check=True,
    )
    if ad_hoc_sign:
        # ad hoc signature is represented by a dash
        # https://developer.apple.com/documentation/security/seccodesignatureflags/kseccodesignatureadhoc
        replace_signature(filename, "-") @contextmanager
def TempDirWithoutEnvVars(*env_vars):
    """Remove `env_vars` from the environment and restore them after
    testing is complete.
    """
    old_vars = {}
    for var in env_vars:
        old_vars[var] = os.environ.get(var, None)
        if old_vars[var] is not None:
            del os.environ[var]
    try:
        with InTemporaryDirectory() as tmpdir:
            yield tmpdir
    finally:
        for var in old_vars:
            if old_vars[var] is not None:
                os.environ[var] = old_vars[var]
            else:
                if var in os.environ:
                    del os.environ[var] def assert_equal(first, second):
    __tracebackhide__ = True
    assert first == second def assert_raises(expected_exception, *args, **kwargs):
    __tracebackhide__ = True
    return pytest.raises(expected_exception, *args, **kwargs) EXT_LIBS = (LIBSTDCXX, LIBSYSTEMB) LIBA = pjoin(DATA_PATH, "liba.dylib") LIBB = pjoin(DATA_PATH, "libb.dylib") LIBC = pjoin(DATA_PATH, "libc.dylib") TEST_LIB = pjoin(DATA_PATH, "test-lib") def _copy_libs(lib_files, out_path):
    copied = []
    if not exists(out_path):
        os.makedirs(out_path)
    for in_fname in lib_files:
        out_fname = pjoin(out_path, basename(in_fname))
        shutil.copy2(in_fname, out_fname)
        copied.append(out_fname)
    return copied ARCH_32 = frozenset(["i386"]) ARCH_64 = frozenset(["x86_64"]) ARCH_BOTH = ARCH_64 | ARCH_M1 ARCH_M1 = frozenset(["arm64"]) LIB64 = pjoin(DATA_PATH, "liba.dylib") LIB64A = pjoin(DATA_PATH, "liba.a") LIBBOTH = pjoin(DATA_PATH, "liba_both.dylib") LIBM1 = pjoin(DATA_PATH, "libam1.dylib")
import os
import shutil
import subprocess
import pytest
from collections import namedtuple
from os.path import basename, dirname
from os.path import join as pjoin
from os.path import realpath, relpath, splitext
from typing import Any, Callable, Dict, Iterable, List, Set, Text, Tuple
from ..delocating import (
    DelocationError,
    bads_report,
    check_archs,
    copy_recurse,
    delocate_path,
    delocate_tree_libs,
    filter_system_libs,
)
from ..libsana import (
    search_environment_for_lib,
    tree_libs,
    tree_libs_from_directory,
)
from ..tmpdirs import InTemporaryDirectory
from ..tools import get_install_names, set_install_name
from .env_tools import TempDirWithoutEnvVars
from .pytest_tools import assert_equal, assert_raises
from .test_install_names import EXT_LIBS, LIBA, LIBB, LIBC, TEST_LIB, _copy_libs
from .test_tools import (
    ARCH_32,
    ARCH_64,
    ARCH_BOTH,
    ARCH_M1,
    LIB64,
    LIB64A,
    LIBBOTH,
    LIBM1,
)
        {(LIB64, LIBM1, ARCH_M1)},
    )
    # For single archs depending, dual archs in depended is OK
    assert check_archs({LIBBOTH: {LIB64A: "install_name"}}) == s0
    # For dual archs in depending, both must be present
    assert_equal(check_archs({LIBBOTH: {LIBBOTH: "install_name"}}), s0)
    assert_equal(
        check_archs({LIB64A: {LIBBOTH: "install_name"}}),
        {(LIB64A, LIBBOTH, ARCH_M1)},
    )
    # More than one bad
    in_dict = {
        LIB64A: {LIBBOTH: "install_name"},
        LIB64: {LIBM1: "install_name"},
    }
    exp_res = {(LIB64A, LIBBOTH, ARCH_M1), (LIB64, LIBM1, ARCH_M1)}
    assert_equal(check_archs(in_dict), exp_res)
    # Check stop_fast flag; can't predict return, but there should only be one
    stopped = check_archs(in_dict, (), True)
    assert_equal(len(stopped), 1)
    # More than one bad in dependings
    assert_equal(
        check_archs(
            {
                LIB64A: {LIBBOTH: "install_name", LIBM1: "install_name"},
                LIB64: {LIBM1: "install_name"},
            }
        ),
        {
            (LIB64A, LIBBOTH, ARCH_M1),
            (LIB64A, LIBM1, ARCH_M1),
            (LIB64, LIBM1, ARCH_M1),
        },
    )


def test_bads_report():
    # type: () -> None
    # Test bads_report of architecture errors
    # No bads, no report
    assert_equal(bads_report(set()), "")
    fmt_str_2 = "Required arch arm64 missing from {0}"
    fmt_str_3 = "{0} needs arch arm64 missing from {1}"
    # One line report
    assert_equal(
        bads_report({(LIB64, LIBM1, ARCH_M1)}), fmt_str_3.format(LIBM1, LIB64)
    )
    # One line report applying path stripper
    assert_equal(
        bads_report({(LIB64, LIBM1, ARCH_M1)}, dirname(LIB64)),
        fmt_str_3.format(basename(LIBM1), basename(LIB64)),
    )
    # Multi-line report
    report = bads_report(
        {
            (LIB64A, LIBBOTH, ARCH_M1),
            (LIB64A, LIBM1, ARCH_M1),
            (LIB64, LIBM1, ARCH_M1),
        }
    )
    expected = {
        fmt_str_3.format(LIBM1, LIB64A),
        fmt_str_3.format(LIBM1, LIB64),
        fmt_str_3.format(LIBBOTH, LIB64A),
    }
    # Set ordering undefined.
    assert_equal(set(report.splitlines()), expected)
    # Two tuples and three tuples
    report2 = bads_report(
        {(LIB64A, LIBBOTH, ARCH_M1), (LIB64, ARCH_M1), (LIBM1, ARCH_M1)}
    )
    expected2 = {
        fmt_str_3.format(LIBBOTH, LIB64A),
        fmt_str_2.format(LIB64),
        fmt_str_2.format(LIBM1),
    }
    assert_equal(set(report2.splitlines()), expected2)
    # Tuples must be length 2 or 3
    assert_raises(
        ValueError,
        bads_report,
        {(LIB64A, LIBBOTH, ARCH_M1), (LIB64,), (LIBM1, ARCH_M1)},
    )
    # Tuples must be length 2 or 3
    assert_raises(
        ValueError,
        bads_report,
        {
            (LIB64A, LIBBOTH, ARCH_M1),
            (LIB64, LIB64, ARCH_M1, ARCH_64),
            (LIBM1, ARCH_M1),
        },
    )


def test_dyld_library_path_lookups() -> None:
    # Test that DYLD_LIBRARY_PATH can be used to find libs during
    # delocation
    with TempDirWithoutEnvVars("DYLD_LIBRARY_PATH") as tmpdir:
        # Copy libs into a temporary directory
        subtree = pjoin(tmpdir, "subtree")
        all_local_libs = _make_libtree(subtree)
        liba, libb, libc, test_lib, slibc, stest_lib = all_local_libs
        # move libb and confirm that test_lib doesn't work
        hidden_dir = "hidden"
        os.mkdir(hidden_dir)
        new_libb = os.path.join(hidden_dir, os.path.basename(LIBB))
        shutil.move(libb, new_libb)
        with pytest.raises(subprocess.CalledProcessError):
            subprocess.run([test_lib], check=True)
        # Update DYLD_LIBRARY_PATH and confirm that we can now
        # successfully delocate test_lib
        os.environ["DYLD_LIBRARY_PATH"] = hidden_dir
        delocate_path("subtree", "deplibs")
        subprocess.run([test_lib], check=True)


def test_dyld_library_path_beats_basename():
    # type: () -> None
    # Test that we find libraries on DYLD_LIBRARY_PATH before basename

    with TempDirWithoutEnvVars("DYLD_LIBRARY_PATH") as tmpdir: