class InGivenDirectory(object):
    """Change directory to given directory for duration of ``with`` block

    Useful when you want to use `InTemporaryDirectory` for the final test, but
    you are still debugging.  For example, you may want to do this in the end:

    >>> with InTemporaryDirectory() as tmpdir:
    ...     # do something complicated which might break
    ...     pass

    But indeed the complicated thing does break, and meanwhile the
    ``InTemporaryDirectory`` context manager wiped out the directory with the
    temporary files that you wanted for debugging.  So, while debugging, you
    replace with something like:

    >>> with InGivenDirectory() as tmpdir: # Use working directory by default
    ...     # do something complicated which might break
    ...     pass

    You can then look at the temporary file outputs to debug what is happening,
    fix, and finally replace ``InGivenDirectory`` with ``InTemporaryDirectory``
    again.
    """

    def __init__(self, path=None):
        """Initialize directory context manager

        Parameters
        ----------
        path : None or str, optional
            path to change directory to, for duration of ``with`` block.
            Defaults to ``os.getcwd()`` if None
        """
        if path is None:
            path = os.getcwd()
        self.path = os.path.abspath(path)

    def __enter__(self):
        self._pwd = os.path.abspath(os.getcwd())
        if not os.path.isdir(self.path):
            os.mkdir(self.path)
        os.chdir(self.path)
        return self.path

    def __exit__(self, exc, value, tb):
        os.chdir(self._pwd) 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 dir2zip(in_dir, zip_fname):
    """Make a zip file `zip_fname` with contents of directory `in_dir`

    The recorded filenames are relative to `in_dir`, so doing a standard zip
    unpack of the resulting `zip_fname` in an empty directory will result in
    the original directory contents.

    Parameters
    ----------
    in_dir : str
        Directory path containing files to go in the zip archive
    zip_fname : str
        Filename of zip archive to write
    """
    z = zipfile.ZipFile(zip_fname, "w", compression=zipfile.ZIP_DEFLATED)
    for root, dirs, files in os.walk(in_dir):
        for file in files:
            in_fname = pjoin(root, file)
            in_stat = os.stat(in_fname)
            # Preserve file permissions, but allow copy
            info = zipfile.ZipInfo(in_fname)
            info.filename = relpath(in_fname, in_dir)
            if os.path.sep == "\\":
                # Make the path unix friendly on windows.
                # PyPI won't accept wheels with windows path separators
                info.filename = relpath(in_fname, in_dir).replace("\\", "/")
            # Set time from modification time
            info.date_time = time.localtime(in_stat.st_mtime)
            # See https://stackoverflow.com/questions/434641/how-do-i-set-permissions-attributes-on-a-file-in-a-zip-file-using-pythons-zip/48435482#48435482 # noqa: E501
            # Also set regular file permissions
            perms = stat.S_IMODE(in_stat.st_mode) | stat.S_IFREG
            info.external_attr = perms << 16
            with open_readable(in_fname, "rb") as fobj:
                contents = fobj.read()
            z.writestr(info, contents, zipfile.ZIP_DEFLATED)
    z.close() @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, "-") def zip2dir(zip_fname: str, out_dir: str) -> None:
    """Extract `zip_fname` into output directory `out_dir`

    Parameters
    ----------
    zip_fname : str
        Filename of zip archive to write
    out_dir : str
        Directory path containing files to go in the zip archive
    """
    # Use unzip command rather than zipfile module to preserve permissions
    # http://bugs.python.org/issue15795
    subprocess.run(
        ["unzip", "-o", "-d", out_dir, zip_fname],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        check=True,
    ) class InWheel(InTemporaryDirectory):
    """Context manager for doing things inside wheels

    On entering, you'll find yourself in the root tree of the wheel.  If you've
    asked for an output wheel, then on exit we'll rewrite the wheel record and
    pack stuff up for you.
    """

    def __init__(self, in_wheel, out_wheel=None, ret_self=False):
        """Initialize in-wheel context manager

        Parameters
        ----------
        in_wheel : str
            filename of wheel to unpack and work inside
        out_wheel : None or str:
            filename of wheel to write after exiting.  If None, don't write and
            discard
        ret_self : bool, optional
            If True, return ``self`` from ``__enter__``, otherwise return the
            directory path.
        """
        self.in_wheel = abspath(in_wheel)
        self.out_wheel = None if out_wheel is None else abspath(out_wheel)
        super(InWheel, self).__init__()

    def __enter__(self):
        zip2dir(self.in_wheel, self.name)
        return super(InWheel, self).__enter__()

    def __exit__(self, exc, value, tb):
        if self.out_wheel is not None:
            rewrite_record(self.name)
            dir2zip(self.name, self.out_wheel)
        return super(InWheel, self).__exit__(exc, value, tb) def assert_equal(first, second):
    __tracebackhide__ = True
    assert first == second def assert_false(condition):
    __tracebackhide__ = True
    assert not condition def assert_raises(expected_exception, *args, **kwargs):
    __tracebackhide__ = True
    return pytest.raises(expected_exception, *args, **kwargs) def assert_true(condition):
    __tracebackhide__ = True
    assert condition class ScriptRunner(object):
    """Class to run scripts and return output

    Finds local scripts and local modules if running in the development
    directory, otherwise finds system scripts and modules.
    """

    def __init__(
        self,
        script_sdir="scripts",
        module_sdir=MY_PACKAGE,
        debug_print_var=None,
        output_processor=lambda x: x,
    ):
        """Init ScriptRunner instance

        Parameters
        ----------
        script_sdir : str, optional
            Name of subdirectory in top-level directory (directory containing
            setup.py), to find scripts in development tree.  Typically
            'scripts', but might be 'bin'.
        module_sdir : str, optional
            Name of subdirectory in top-level directory (directory containing
            setup.py), to find main package directory.
        debug_print_vsr : str, optional
            Name of environment variable that indicates whether to do debug
            printing or no.
        output_processor : callable
            Callable to run on the stdout, stderr outputs before returning
            them.  Use this to convert bytes to unicode, strip whitespace, etc.
        """
        self.local_script_dir = local_script_dir(script_sdir)
        self.local_module_dir = local_module_dir(module_sdir)
        if debug_print_var is None:
            debug_print_var = "{0}_DEBUG_PRINT".format(module_sdir.upper())
        self.debug_print = os.environ.get(debug_print_var, False)
        self.output_processor = output_processor

    def run_command(self, cmd, check_code=True):
        """Run command sequence `cmd` returning exit code, stdout, stderr

        Parameters
        ----------
        cmd : str or sequence
            string with command name or sequence of strings defining command
        check_code : {True, False}, optional
            If True, raise error for non-zero return code

        Returns
        -------
        returncode : int
            return code from execution of `cmd`
        stdout : bytes (python 3) or str (python 2)
            stdout from `cmd`
        stderr : bytes (python 3) or str (python 2)
            stderr from `cmd`
        """
        if isinstance(cmd, str):
            cmd = [cmd]
        else:
            cmd = list(cmd)
        if self.local_script_dir is not None:
            # Windows can't run script files without extensions natively
            # so we need to run local scripts (no extensions) via the
            # Python interpreter. On Unix, we might have the wrong incantation
            # for the Python interpreter in the hash bang first line in the
            # source file.  So, either way, run the script through the
            # Python interpreter
            cmd = [sys.executable, pjoin(self.local_script_dir, cmd[0])] + cmd[
                1:
            ]
        elif os.name == "nt":
            # Need .bat file extension for windows
            cmd[0] += ".bat"
        if os.name == "nt":
            # Quote any arguments with spaces. The quotes delimit the arguments
            # on Windows, and the arguments might be files paths with spaces.
            # On Unix the list elements are each separate arguments.
            cmd = ['"{0}"'.format(c) if " " in c else c for c in cmd]
        if self.debug_print:
            print("Running command '%s'" % cmd)
        env = os.environ
        if self.local_module_dir is not None:
            # module likely comes from the current working directory.
            # We might need that directory on the path if we're running
            # the scripts from a temporary directory
            env = env.copy()
            pypath = env.get("PYTHONPATH", None)
            if pypath is None:
                env["PYTHONPATH"] = self.local_module_dir
            else:
                env["PYTHONPATH"] = self.local_module_dir + pathsep + pypath
        proc = Popen(cmd, stdout=PIPE, stderr=PIPE, env=env)
        stdout, stderr = proc.communicate()
        if proc.poll() is None:
            proc.terminate()
        if check_code and proc.returncode != 0:
            raise RuntimeError(
                """Command "{0}" failed with
                stdout
                ------
                {1}
                stderr
                ------
                {2}
                """.format(
                    cmd, stdout, stderr
                )
            )
        opp = self.output_processor
        return proc.returncode, opp(stdout), opp(stderr) def _copy_to(fname: str, directory: str, new_base: str) -> str:
    new_name = pjoin(directory, new_base)
    shutil.copy2(fname, new_name)
    return new_name def _make_bare_depends():
    # type: () -> Tuple[Text, Text]
    # Copy:
    # * liba.dylib to 'libs' dir, which is a dependency of libb.dylib
    # * libb.dylib to 'subtree' dir, as 'libb' (no extension).
    #
    # This is for testing delocation when the depending file does not have a
    # dynamic library file extension.
    (libb,) = _copy_libs([LIBB], "subtree")
    (liba,) = _copy_libs([LIBA], "libs")
    bare_b, _ = splitext(libb)
    os.rename(libb, bare_b)
    # use realpath for OSX /private/var - /var
    set_install_name(bare_b, "liba.dylib", realpath(liba))
    return liba, bare_b def _make_libtree(out_path: str) -> LibtreeLibs:
    liba, libb, libc, test_lib = _copy_libs(
        [LIBA, LIBB, LIBC, TEST_LIB], out_path
    )
    sub_path = pjoin(out_path, "subsub")
    slibc, stest_lib = _copy_libs([libc, test_lib], sub_path)
    # Set execute permissions
    for exe in (test_lib, stest_lib):
        os.chmod(exe, 0o744)
    # Check test-lib doesn't work because of relative library paths
    with pytest.raises(subprocess.CalledProcessError):
        subprocess.run([test_lib], check=True)
    with pytest.raises(subprocess.CalledProcessError):
        subprocess.run([stest_lib], check=True)
    # Fixup the relative path library names by setting absolute paths
    for fname, using, path in (
        (libb, "liba.dylib", out_path),
        (libc, "liba.dylib", out_path),
        (libc, "libb.dylib", out_path),
        (test_lib, "libc.dylib", out_path),
        (slibc, "liba.dylib", out_path),
        (slibc, "libb.dylib", out_path),
        (stest_lib, "libc.dylib", sub_path),
    ):
        set_install_name(fname, using, pjoin(path, using))
    # Check scripts now execute correctly
    subprocess.run([test_lib], check=True)
    subprocess.run([stest_lib], check=True)
    return LibtreeLibs(liba, libb, libc, test_lib, slibc, stest_lib) def assert_same_tree(tree1: str, tree2: str) -> None:
    for dirpath, dirnames, filenames in os.walk(tree1):
        tree2_dirpath = pjoin(tree2, relpath(dirpath, tree1))
        for dname in dirnames:
            assert isdir(pjoin(tree2_dirpath, dname))
        for fname in filenames:
            tree1_path = pjoin(dirpath, fname)
            with open_readable(tree1_path, "rb") as fobj:
                contents1 = fobj.read()
            with open_readable(pjoin(tree2_dirpath, fname), "rb") as fobj:
                contents2 = fobj.read()
            if fname == "RECORD":  # Record can have different line orders
                assert_record_equal(contents1, contents2)
            else:
                assert contents1 == contents2 EXT_LIBS = (LIBSTDCXX, LIBSYSTEMB) PLAT_WHEEL = _collect_wheel("fakepkg1-1.0-cp*.whl") PURE_WHEEL = _collect_wheel("fakepkg2-1.0-py*.whl") WHEEL_PATCH = pjoin(DATA_PATH, "fakepkg2.patch") WHEEL_PATCH_BAD = pjoin(DATA_PATH, "fakepkg2.bad_patch") class PlatWheel(NamedTuple):
    """Information about a temporary platform wheel."""

    whl: str  # Path to the wheel.
    stray_lib: str  # Path to the external library. def _fixed_wheel(out_path):
    wheel_base = basename(PLAT_WHEEL)
    with InGivenDirectory(out_path):
        zip2dir(PLAT_WHEEL, "_plat_pkg")
        if not exists("_libs"):
            os.makedirs("_libs")
        shutil.copy2(STRAY_LIB, "_libs")
        stray_lib = pjoin(abspath(realpath("_libs")), basename(STRAY_LIB))
        requiring = pjoin("_plat_pkg", "fakepkg1", "subpkg", "module2.abi3.so")
        old_lib = set(get_install_names(requiring)).difference(EXT_LIBS).pop()
        set_install_name(requiring, old_lib, stray_lib)
        dir2zip("_plat_pkg", wheel_base)
        shutil.rmtree("_plat_pkg")
    return pjoin(out_path, wheel_base), stray_lib def _rename_module(in_wheel, mod_fname, out_wheel):
    # Rename module with library dependency in wheel
    with InWheel(in_wheel, out_wheel):
        mod_dir = pjoin("fakepkg1", "subpkg")
        os.rename(pjoin(mod_dir, "module2.abi3.so"), pjoin(mod_dir, mod_fname))
    return out_wheel def _thin_lib(stray_lib, arch):
    check_call(["lipo", "-thin", arch, stray_lib, "-output", stray_lib]) def _thin_mod(wheel, arch):
    with InWheel(wheel, wheel):
        mod_fname = pjoin("fakepkg1", "subpkg", "module2.abi3.so")
        check_call(["lipo", "-thin", arch, mod_fname, "-output", mod_fname]) EXP_ITEMS = [
    ("Generator", "bdist_wheel {pip_version}"),
    ("Root-Is-Purelib", "false"),
    ("Tag", "{pyver}-{abi}-" + EXP_PLAT),
    ("Wheel-Version", "1.0"),
] EXTRA_EXPS = [
    ("Generator", "bdist_wheel {pip_version}"),
    ("Root-Is-Purelib", "false"),
] + [("Tag", "{pyver}-{abi}-" + plat) for plat in (EXP_PLAT,) + EXTRA_PLATS] EXTRA_PLATS = ("macosx_10_11_universal2", "macosx_10_11_x86_64") def assert_winfo_similar(whl_fname, exp_items, drop_version=True):
    wf = WheelFile(whl_fname)
    wheel_parts = wf.parsed_filename.groupdict()
    # Info can contain duplicate keys (e.g. Tag)
    w_info = sorted(get_info(wf).items())
    if drop_version:
        w_info = _filter_key(w_info, "Wheel-Version")
        exp_items = _filter_key(exp_items, "Wheel-Version")
    assert_equal(len(exp_items), len(w_info))
    # Extract some information from actual values
    wheel_parts["pip_version"] = dict(w_info)["Generator"].split()[1]
    for (key1, value1), (key2, value2) in zip(
        sorted(exp_items), sorted(w_info)
    ):
        assert_equal(key1, key2)
        value1 = value1.format(**wheel_parts)
        assert_equal(value1, value2)
import os
import shutil
import subprocess
from os.path import abspath, basename, dirname, exists, isfile
from os.path import join as pjoin
from os.path import realpath, splitext
from typing import Text
from ..tmpdirs import InGivenDirectory, InTemporaryDirectory
from ..tools import dir2zip, set_install_name, zip2dir
from ..wheeltools import InWheel
from .pytest_tools import assert_equal, assert_false, assert_raises, assert_true
from .scriptrunner import ScriptRunner
from .test_delocating import _copy_to, _make_bare_depends, _make_libtree
from .test_fuse import assert_same_tree
from .test_install_names import EXT_LIBS
from .test_wheelies import (
    PLAT_WHEEL,
    PURE_WHEEL,
    WHEEL_PATCH,
    WHEEL_PATCH_BAD,
    PlatWheel,
    _fixed_wheel,
    _rename_module,
    _thin_lib,
    _thin_mod,
)
from .test_wheeltools import (
    EXP_ITEMS,
    EXTRA_EXPS,
    EXTRA_PLATS,
    assert_winfo_similar,
)
                "fixed4",
                fixed_wheel,
                "wheel_copy.ext",
            ]
        )
        wheel_lines2 = [
            "Fixing: wheel_copy.ext",
            "Copied to package .dylibs directory:",
            stray_lib,
        ]
        assert_equal(stdout, wheel_lines1 + wheel_lines2)


def test_fix_wheel_dylibs():
    # Check default and non-default search for dynamic libraries
    with InTemporaryDirectory() as tmpdir:
        # Default in-place fix
        fixed_wheel, stray_lib = _fixed_wheel(tmpdir)
        _rename_module(fixed_wheel, "module.other", "test.whl")
        shutil.copyfile("test.whl", "test2.whl")
        # Default is to look in all files and therefore fix
        code, stdout, stderr = run_command(["delocate-wheel", "test.whl"])
        _check_wheel("test.whl", ".dylibs")
        # Can turn this off to only look in dynamic lib exts
        code, stdout, stderr = run_command(
            ["delocate-wheel", "test2.whl", "-d"]
        )
        with InWheel("test2.whl"):  # No fix
            assert_false(exists(pjoin("fakepkg1", ".dylibs")))


def test_fix_wheel_archs():
    # type: () -> None
    # Some tests for wheel fixing
    with InTemporaryDirectory() as tmpdir:
        # Test check of architectures
        fixed_wheel, stray_lib = _fixed_wheel(tmpdir)
        # Fixed wheel, architectures are OK
        code, stdout, stderr = run_command(
            ["delocate-wheel", fixed_wheel, "-k"]
        )
        _check_wheel(fixed_wheel, ".dylibs")
        # Broken with one architecture removed
        archs = set(("x86_64", "arm64"))

        def _fix_break(arch):
            # type: (Text) -> None
            _fixed_wheel(tmpdir)
            _thin_lib(stray_lib, arch)

        def _fix_break_fix(arch):
            # type: (Text) -> None
            _fixed_wheel(tmpdir)
            _thin_lib(stray_lib, arch)
            _thin_mod(fixed_wheel, arch)

        for arch in archs:
            # Not checked
            _fix_break(arch)
            code, stdout, stderr = run_command(["delocate-wheel", fixed_wheel])
            _check_wheel(fixed_wheel, ".dylibs")
            # Checked
            _fix_break(arch)
            code, stdout, stderr = bytes_runner.run_command(
                ["delocate-wheel", fixed_wheel, "--check-archs"],
                check_code=False,
            )
            assert_false(code == 0)
            stderr_unicode = stderr.decode("latin1").strip()
            assert stderr_unicode.startswith("Traceback")
            assert (
                "DelocationError: Some missing architectures in wheel"
                in stderr_unicode
            )
            assert_equal(stdout.strip(), b"")
            # Checked, verbose
            _fix_break(arch)
            code, stdout, stderr = bytes_runner.run_command(
                ["delocate-wheel", fixed_wheel, "--check-archs", "-v"],
                check_code=False,
            )
            assert_false(code == 0)
            stderr = stderr.decode("latin1").strip()
            assert "Traceback" in stderr
            assert stderr.endswith(
                "DelocationError: Some missing architectures in wheel"
                f"\n{'fakepkg1/subpkg/module2.abi3.so'}"
                f" needs arch {archs.difference([arch]).pop()}"
                f" missing from {stray_lib}"
            )
            stdout_unicode = stdout.decode("latin1").strip()
            assert stdout_unicode == f"Fixing: {fixed_wheel}"
            # Require particular architectures
        both_archs = "arm64,x86_64"
        for ok in ("universal2", "arm64", "x86_64", both_archs):
            _fixed_wheel(tmpdir)
            code, stdout, stderr = run_command(
                ["delocate-wheel", fixed_wheel, "--require-archs=" + ok]
            )
        for arch in archs:
            other_arch = archs.difference([arch]).pop()
            for not_ok in ("intel", both_archs, other_arch):
                _fix_break_fix(arch)
                code, stdout, stderr = run_command(
                    [
                        "delocate-wheel",
                        fixed_wheel,
                        "--require-archs=" + not_ok,
                    ],
                    check_code=False,
                )
                assert_false(code == 0)


def test_fuse_wheels():
    # Some tests for wheel fusing
    with InTemporaryDirectory():
        zip2dir(PLAT_WHEEL, "to_wheel")
        zip2dir(PLAT_WHEEL, "from_wheel")
        dir2zip("to_wheel", "to_wheel.whl")

        dir2zip("from_wheel", "from_wheel.whl")