Skip to content

Commit

Permalink
Merge pull request #164 from isuruf/sanitize_rpaths2
Browse files Browse the repository at this point in the history
Option to sanitize rpaths
  • Loading branch information
HexDecimal committed Nov 22, 2023
2 parents 082ab1e + ccc82a1 commit 979c7ef
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 4 deletions.
2 changes: 2 additions & 0 deletions Changelog
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Releases

* Dependencies can be manually excluded with the ``--exclude <name>`` flag.
[#106](https://github.com/matthew-brett/delocate/pull/106)
* Non-portable rpaths can be cleaned up using the ``--sanitize-rpaths`` flag.
[#164](https://github.com/matthew-brett/delocate/pull/164)

* [0.10.5] - 2023-11-14

Expand Down
7 changes: 7 additions & 0 deletions delocate/cmd/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ def delocate_args(parser: OptionParser):
"as much as possible"
),
),
Option(
"--sanitize-rpaths",
action="store_true",
help="Remove absolute and relative rpaths from binaries",
),
]
)

Expand All @@ -91,6 +96,7 @@ class DelocateArgs(TypedDict):
executable_path: str
lib_filt_func: Callable[[str], bool] | Literal["dylibs-only"] | None
ignore_missing: bool
sanitize_rpaths: bool


def delocate_values(opts: Values) -> DelocateArgs:
Expand All @@ -115,4 +121,5 @@ def copy_filter_exclude(name: str) -> bool:
"executable_path": opts.executable_path,
"lib_filt_func": "dylibs-only" if opts.dylibs_only else None,
"ignore_missing": opts.ignore_missing_dependencies,
"sanitize_rpaths": opts.sanitize_rpaths,
}
37 changes: 34 additions & 3 deletions delocate/delocating.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
)
from .tmpdirs import TemporaryDirectory
from .tools import (
_remove_absolute_rpaths,
dir2zip,
find_package_dirs,
get_archs,
Expand All @@ -59,6 +60,8 @@ 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`
Expand Down Expand Up @@ -86,6 +89,8 @@ def delocate_tree_libs(
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
-------
Expand All @@ -111,9 +116,24 @@ def delocate_tree_libs(
_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,
Expand Down Expand Up @@ -403,6 +423,8 @@ def delocate_path(
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`
Expand All @@ -429,6 +451,8 @@ def delocate_path(
`@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
-------
Expand Down Expand Up @@ -471,7 +495,9 @@ def delocate_path(
ignore_missing=ignore_missing,
)

return delocate_tree_libs(lib_dict, lib_path, tree_path)
return delocate_tree_libs(
lib_dict, lib_path, tree_path, sanitize_rpaths=sanitize_rpaths
)


def _copy_lib_dict(
Expand Down Expand Up @@ -560,8 +586,10 @@ def delocate_wheel(
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,
) -> Dict[str, Dict[str, str]]:
"""Update wheel by copying required libraries to `lib_sdir` in wheel
Expand All @@ -580,8 +608,8 @@ def delocate_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.
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
Expand All @@ -607,6 +635,8 @@ def delocate_wheel(
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.
Returns
-------
Expand Down Expand Up @@ -649,6 +679,7 @@ def delocate_wheel(
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(
Expand Down
33 changes: 32 additions & 1 deletion delocate/tests/test_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@
from pytest_console_scripts import ScriptRunner

from ..tmpdirs import InGivenDirectory, InTemporaryDirectory
from ..tools import dir2zip, set_install_name, zip2dir
from ..tools import dir2zip, get_rpaths, set_install_name, zip2dir
from ..wheeltools import InWheel
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,
RPATH_WHEEL,
WHEEL_PATCH,
WHEEL_PATCH_BAD,
PlatWheel,
Expand Down Expand Up @@ -594,3 +595,33 @@ def test_fix_wheel_with_excluded_dylibs(script_runner: ScriptRunner):
["delocate-wheel", "-e", "doesnotexist", "test2.whl"], check=True
)
_check_wheel("test2.whl", ".dylibs")


@pytest.mark.xfail( # type: ignore[misc]
sys.platform != "darwin", reason="Needs macOS linkage."
)
def test_sanitize_command(tmp_path: Path, script_runner: ScriptRunner) -> None:
unpack_dir = tmp_path / "unpack"
zip2dir(RPATH_WHEEL, unpack_dir)
assert "libs/" in set(
get_rpaths(str(unpack_dir / "fakepkg/subpkg/module2.abi3.so"))
)

rpath_wheel = tmp_path / "example.whl"
shutil.copyfile(RPATH_WHEEL, rpath_wheel)
libs_path = tmp_path / "libs"
libs_path.mkdir()
shutil.copy(DATA_PATH / "libextfunc_rpath.dylib", libs_path)
shutil.copy(DATA_PATH / "libextfunc2_rpath.dylib", libs_path)
result = script_runner.run(
["delocate-wheel", "-vv", "--sanitize-rpaths", rpath_wheel],
check=True,
cwd=tmp_path,
)
assert "Sanitize: Deleting rpath 'libs/' from" in result.stderr

unpack_dir = tmp_path / "unpack"
zip2dir(rpath_wheel, unpack_dir)
assert "libs/" not in set(
get_rpaths(str(unpack_dir / "fakepkg/subpkg/module2.abi3.so"))
)
55 changes: 55 additions & 0 deletions delocate/tools.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
""" Tools for getting and setting install names """
from __future__ import annotations

import logging
import os
import re
import stat
Expand Down Expand Up @@ -28,6 +29,8 @@

T = TypeVar("T")

logger = logging.getLogger(__name__)


class InstallNameError(Exception):
pass
Expand Down Expand Up @@ -814,6 +817,58 @@ def add_rpath(filename: str, newpath: str, ad_hoc_sign: bool = True) -> None:
replace_signature(filename, "-")


_SANITARY_RPATH = re.compile(r"^@loader_path/|^@executable_path/")
"""Matches rpaths which are considered sanitary."""


def _is_rpath_sanitary(rpath: str) -> bool:
"""Returns True if `rpath` is considered sanitary.
Includes only paths relative to `@executable_path` or `@loader_path`.
Excludes absolute and relative (to the current directory) paths.
Examples
--------
>>> _is_rpath_sanitary("/absolute/path")
False
>>> _is_rpath_sanitary("relative/path")
False
>>> _is_rpath_sanitary("@loader_path/../example")
True
>>> _is_rpath_sanitary("@executable_path/../example")
True
>>> _is_rpath_sanitary("fake/@loader_path/../example")
False
>>> _is_rpath_sanitary("@other_path/../example")
False
"""
return bool(_SANITARY_RPATH.match(rpath))


@ensure_writable
def _remove_absolute_rpaths(filename: str, ad_hoc_sign: bool = True) -> None:
"""Remove absolute filename rpaths in `filename`
Parameters
----------
filename : str
filename of library
ad_hoc_sign : {True, False}, optional
If True, sign file with ad-hoc signature
"""
commands = [] # install_name_tool commands
for rpath in get_rpaths(filename):
if not _is_rpath_sanitary(rpath):
commands += ["-delete_rpath", rpath]
logger.info("Sanitize: Deleting rpath %r from %r", rpath, filename)
if not commands:
return
_run(["install_name_tool", filename, *commands], check=True)
if ad_hoc_sign:
replace_signature(filename, "-")


def zip2dir(
zip_fname: str | PathLike[str], out_dir: str | PathLike[str]
) -> None:
Expand Down

0 comments on commit 979c7ef

Please sign in to comment.