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) class InstallNameError(Exception):
    pass @ensure_writable
def add_rpath(filename: str, newpath: str, ad_hoc_sign: bool = True) -> None:
    """Add rpath `newpath` to library `filename`

    Parameters
    ----------
    filename : str
        filename of library
    newpath : str
        rpath to add
    ad_hoc_sign : {True, False}, optional
        If True, sign file with ad-hoc signature
    """
    subprocess.run(
        ["install_name_tool", "-add_rpath", newpath, filename],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        check=True,
    )
    if ad_hoc_sign:
        replace_signature(filename, "-") def get_environment_variable_paths():
    """Return a tuple of entries in `DYLD_LIBRARY_PATH` and
    `DYLD_FALLBACK_LIBRARY_PATH`.

    This will allow us to search those locations for dependencies of libraries
    as well as `@rpath` entries.

    Returns
    -------
    env_var_paths : tuple
        path entries in environment variables
    """
    # We'll search the extra library paths in a specific order:
    # DYLD_LIBRARY_PATH and then DYLD_FALLBACK_LIBRARY_PATH
    env_var_paths = []
    extra_paths = ["DYLD_LIBRARY_PATH", "DYLD_FALLBACK_LIBRARY_PATH"]
    for pathname in extra_paths:
        path_contents = os.environ.get(pathname)
        if path_contents is not None:
            for path in path_contents.split(":"):
                env_var_paths.append(path)
    return tuple(env_var_paths) def get_install_id(filename: str) -> Optional[str]:
    """Return install id from library named in `filename`

    Returns None if no install id, or if this is not an object file.

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

    Returns
    -------
    install_id : str
        install id of library `filename`, or None if no install id

    Raises
    ------
    NotImplementedError
        If ``filename`` has different install ids per-architecture.
    """
    install_ids = _get_install_ids(filename)
    if not install_ids:
        return None  # No install ids or nothing returned.
    return _check_ignore_archs(install_ids) 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) def get_rpaths(filename: str) -> Tuple[str, ...]:
    """Return a tuple of rpaths from the library `filename`.

    If `filename` is not a library then the returned tuple will be empty.

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

    Returns
    -------
    rpath : tuple
        rpath paths in `filename`

    Raises
    ------
    NotImplementedError
        If ``filename`` has different rpaths 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 ()
    rpaths = _check_ignore_archs(_parse_otool_rpaths(otool.stdout))
    return tuple(rpaths) def parse_install_name(line: str) -> Tuple[str, str, str]:
    """Parse a line of install name output

    Parameters
    ----------
    line : str
        line of install name output from ``otool``

    Returns
    -------
    libname : str
        library install name
    compat_version : str
        compatibility version
    current_version : str
        current version

    Examples
    --------
    >>> parse_install_name("/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 905.6.0)")
    ('/usr/lib/libc++.1.dylib', '1.0.0', '905.6.0')
    """  # noqa: E501
    line = line.strip()
    match = IN_RE.match(line)
    if not match:
        raise ValueError(f"Could not parse {line!r}")
    libname, compat_version, current_version = match.groups()
    return libname, compat_version, current_version @ensure_writable
def set_install_id(filename: str, install_id: str, ad_hoc_sign: bool = True):
    """Set install id for library named in `filename`

    Parameters
    ----------
    filename : str
        filename of library
    install_id : str
        install id for library `filename`
    ad_hoc_sign : {True, False}, optional
        If True, sign library with ad-hoc signature

    Raises
    ------
    RuntimeError if `filename` has not install id
    """
    if get_install_id(filename) is None:
        raise InstallNameError("{0} has no install id".format(filename))
    subprocess.run(
        ["install_name_tool", "-id", install_id, filename],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        check=True,
    )
    if ad_hoc_sign:
        replace_signature(filename, "-") @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)
import os
import shutil
from os.path import basename, dirname, exists
from os.path import join as pjoin
from ..tmpdirs import InTemporaryDirectory
from ..tools import (
    InstallNameError,
    add_rpath,
    get_environment_variable_paths,
    get_install_id,
    get_install_names,
    get_rpaths,
    parse_install_name,
    set_install_id,
    set_install_name,
)
from .env_tools import TempDirWithoutEnvVars
from .pytest_tools import assert_equal, assert_raises
    assert set(get_install_names(LIBA)) == set(EXT_LIBS)
    assert set(get_install_names(LIBB)) == set(("liba.dylib",) + EXT_LIBS)
    assert set(get_install_names(LIBC)) == set(
        ("liba.dylib", "libb.dylib") + EXT_LIBS
    )
    assert set(get_install_names(TEST_LIB)) == set(("libc.dylib",) + EXT_LIBS)
    assert set(get_install_names(LIBAM1_ARCH)) == set(EXT_LIBS)
    # Non-object file returns empty tuple
    assert get_install_names(__file__) == ()
    # Static archive and object files returns empty tuple
    assert get_install_names(A_OBJECT) == ()
    assert get_install_names(LIBA_STATIC) == ()
    # ico file triggers another error message and should also return an empty tuple  # noqa: E501
    assert get_install_names(ICO_FILE) == ()
    # Python file (__file__ above may be a pyc file)
    assert get_install_names(PY_FILE) == ()
    # Binary file (in fact a truncated SAS file)
    assert get_install_names(BIN_FILE) == ()
    # Test when no read permission
    with InTemporaryDirectory():
        shutil.copyfile(LIBA, "test.dylib")
        assert set(get_install_names("test.dylib")) == set(EXT_LIBS)
        # No permissions, no found libs
        os.chmod("test.dylib", 0)
        assert get_install_names("test.dylib") == ()


def test_parse_install_name():
    assert_equal(
        parse_install_name(
            "liba.dylib (compatibility version 0.0.0, current version 0.0.0)"
        ),
        ("liba.dylib", "0.0.0", "0.0.0"),
    )
    assert_equal(
        parse_install_name(
            " /usr/lib/libstdc++.6.dylib (compatibility version 1.0.0, "
            "current version 120.0.0)"
        ),
        ("/usr/lib/libstdc++.6.dylib", "1.0.0", "120.0.0"),
    )
    assert_equal(
        parse_install_name(
            "\t\t   /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, "
            "current version 1197.1.1)"
        ),
        ("/usr/lib/libSystem.B.dylib", "1.0.0", "1197.1.1"),
    )


def test_install_id():
    # Test basic otool library listing
    assert_equal(get_install_id(LIBA), "liba.dylib")
    assert_equal(get_install_id(LIBB), "libb.dylib")
    assert_equal(get_install_id(LIBC), "libc.dylib")
    assert_equal(get_install_id(TEST_LIB), None)
    # Non-object file returns None too
    assert_equal(get_install_id(__file__), None)
    assert_equal(get_install_id(ICO_FILE), None)


def test_change_install_name():
    # Test ability to change install names in library
    libb_names = get_install_names(LIBB)
    with InTemporaryDirectory() as tmpdir:
        libfoo = pjoin(tmpdir, "libfoo.dylib")
        shutil.copy2(LIBB, libfoo)
        assert_equal(get_install_names(libfoo), libb_names)
        set_install_name(libfoo, "liba.dylib", "libbar.dylib")
        assert_equal(
            get_install_names(libfoo), ("libbar.dylib",) + libb_names[1:]
        )
        # If the name not found, raise an error
        assert_raises(
            InstallNameError,
            set_install_name,
            libfoo,
            "liba.dylib",
            "libpho.dylib",
        )


def test_set_install_id():
    # Test ability to change install id in library
    liba_id = get_install_id(LIBA)
    with InTemporaryDirectory() as tmpdir:
        libfoo = pjoin(tmpdir, "libfoo.dylib")
        shutil.copy2(LIBA, libfoo)
        assert_equal(get_install_id(libfoo), liba_id)
        set_install_id(libfoo, "libbar.dylib")
        assert_equal(get_install_id(libfoo), "libbar.dylib")
    # If no install id, raise error (unlike install_name_tool)
    assert_raises(InstallNameError, set_install_id, TEST_LIB, "libbof.dylib")


def test_get_rpaths():
    # Test fetch of rpaths
    # Not dynamic libs, no rpaths
    for fname in (LIBB, A_OBJECT, LIBA_STATIC, ICO_FILE, PY_FILE, BIN_FILE):
        assert_equal(get_rpaths(fname), ())


def test_get_environment_variable_paths():
    # Test that environment variable paths are fetched in a specific order
    with TempDirWithoutEnvVars(
        "DYLD_FALLBACK_LIBRARY_PATH", "DYLD_LIBRARY_PATH"
    ):
        os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = "three"
        os.environ["DYLD_LIBRARY_PATH"] = "two"
        assert_equal(get_environment_variable_paths(), ("two", "three"))


def test_add_rpath():
    # Test adding to rpath
    with InTemporaryDirectory() as tmpdir:
        libfoo = pjoin(tmpdir, "libfoo.dylib")
        shutil.copy2(LIBB, libfoo)
        assert_equal(get_rpaths(libfoo), ())
        add_rpath(libfoo, "/a/path")
        assert_equal(get_rpaths(libfoo), ("/a/path",))

        add_rpath(libfoo, "/another/path")