├── .isort.cfg ├── requirements.txt ├── lddwrap ├── py.typed ├── main.py └── __init__.py ├── mypy.ini ├── docs ├── source │ ├── readme.rst │ ├── changelog.rst │ ├── lddwrap.rst │ ├── index.rst │ └── conf.py └── Makefile ├── requirements-doc.txt ├── bin └── pylddwrap ├── style.yapf ├── .gitignore ├── MANIFEST.in ├── pylint.rc ├── pylddwrap_meta.py ├── CHANGELOG.rst ├── .github └── workflows │ ├── publish-to-pypi.yml │ └── ci.yml ├── LICENSE ├── tox.ini ├── tests ├── __init__.py ├── test_main.py └── test_ldd.py ├── precommit.py ├── setup.py └── README.rst /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | line_length=80 3 | not_skip=__init__.py 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | typing-extensions>=3.6.6 2 | icontract>=2.0.1,<3 3 | -------------------------------------------------------------------------------- /lddwrap/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561. The mypy package uses inline types. 2 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | 3 | [mypy-setuptools.*] 4 | ignore_missing_imports = True 5 | -------------------------------------------------------------------------------- /docs/source/readme.rst: -------------------------------------------------------------------------------- 1 | ****** 2 | README 3 | ****** 4 | .. include:: ../../README.rst 5 | -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | ********* 2 | CHANGELOG 3 | ********* 4 | 5 | .. include:: ../../CHANGELOG.rst 6 | -------------------------------------------------------------------------------- /docs/source/lddwrap.rst: -------------------------------------------------------------------------------- 1 | ******* 2 | lddwrap 3 | ******* 4 | 5 | .. automodule:: lddwrap 6 | :members: 7 | 8 | -------------------------------------------------------------------------------- /requirements-doc.txt: -------------------------------------------------------------------------------- 1 | sphinx>=1.8.1,<2 2 | sphinx-autodoc-typehints>=1.3.0,<2 3 | sphinx-rtd-theme>=0.4.2,<1 4 | sphinx-icontract>=2.0.0,<3 -------------------------------------------------------------------------------- /bin/pylddwrap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Call lddwrap main.""" 3 | import lddwrap.main 4 | 5 | if __name__ == "__main__": 6 | lddwrap.main.main() 7 | -------------------------------------------------------------------------------- /style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | based_on_style = pep8 3 | spaces_before_comment = 2 4 | split_before_logical_operator = true 5 | column_limit = 80 6 | coalesce_brackets = true 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv3 2 | venv 3 | venv-doc 4 | .mypy_cache/ 5 | .idea 6 | *.pyc 7 | .precommit_hashes 8 | *.egg-info 9 | .tox 10 | .coverage 11 | .eggs 12 | dist/ 13 | build/ 14 | 15 | # Sphinx documentation 16 | docs/build/ 17 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.py 2 | include *.rc 3 | include *.rst 4 | include *.txt 5 | include *.yapf 6 | include .isort.cfg 7 | include mypy.ini 8 | include tox.ini 9 | recursive-include docs *.py 10 | recursive-include docs *.rst 11 | recursive-include docs Makefile 12 | recursive-include tests *.py 13 | -------------------------------------------------------------------------------- /pylint.rc: -------------------------------------------------------------------------------- 1 | [TYPECHECK] 2 | ignored-modules = numpy 3 | ignored-classes = numpy,PurePath 4 | generated-members=bottle\.request\.forms\.decode,bottle\.request\.query\.decode 5 | 6 | [FORMAT] 7 | max-line-length=80 8 | 9 | [MESSAGES CONTROL] 10 | disable=too-few-public-methods,abstract-class-little-used,len-as-condition,bad-continuation,bad-whitespace,no-else-return,duplicate-code 11 | -------------------------------------------------------------------------------- /pylddwrap_meta.py: -------------------------------------------------------------------------------- 1 | 2 | """Define meta information about pylddwrap package.""" 3 | 4 | __title__ = 'pylddwrap' 5 | __description__ = 'Wrap ldd *nix utility to determine shared libraries required by a program.' 6 | __url__ = 'http://github.com/Parquery/lddwrap' 7 | __version__ = '1.2.2' # don't forget to update the changelog! 8 | __author__ = 'Selim Naji, Adam Radomski and Marko Ristin' 9 | __author_email__ = 'selim.naji@parquery.com, adam.radomski@parquery.com, marko.ristin@gmail.com' 10 | __license__ = 'MIT' 11 | __copyright__ = 'Copyright 2018 Parquery AG' 12 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. lddwrap documentation master file, created by 2 | sphinx-quickstart on Thu Sep 20 17:36:32 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to pylddwrap's documentation! 7 | ===================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | lddwrap 14 | readme 15 | changelog 16 | 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 1.2.2 2 | ===== 3 | * Added support for paths with spaces (#26) 4 | 5 | 1.2.1 6 | ===== 7 | * Removed support for Python 3.5 (#23) 8 | * Improved support of statically linked libraries (#22) 9 | 10 | 1.2.0 11 | ===== 12 | * Ignore not-indented ldd output (#14) 13 | * Added handling of static libraries (#13) 14 | 15 | 1.1.0 16 | ===== 17 | * Added ``--sorted`` command-line option 18 | * Refactored for easier and more thorough testing 19 | 20 | 1.0.2 21 | ===== 22 | * Added support for Python 3.7 and 3.8 23 | 24 | 1.0.1 25 | ===== 26 | * Fixed license badge in the Readme 27 | 28 | 1.0.0 29 | ===== 30 | * Initial version 31 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = source 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/actions/starter-workflows/blob/main/ci/python-publish.yml 2 | name: Publish to Pypi 3 | 4 | on: 5 | release: 6 | types: [created] 7 | 8 | push: 9 | branches: 10 | - '*/fixed-publishing-to-pypi' 11 | 12 | jobs: 13 | deploy: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Set up Python 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: '3.x' 24 | 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install setuptools wheel twine 29 | 30 | - name: Build and publish 31 | env: 32 | TWINE_USERNAME: "__token__" 33 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 34 | run: | 35 | python setup.py sdist 36 | twine upload dist/* 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Parquery AG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = check,_auto_version 3 | 4 | [testenv] 5 | changedir = {envtmpdir} 6 | extras = 7 | dev 8 | setenv = 9 | _PYTHON_SOURCE_PATHS = {toxinidir}/bin/pylddwrap {toxinidir}/lddwrap/ {toxinidir}/precommit.py {toxinidir}/setup.py 10 | _PYTHON_TEST_PATHS = {toxinidir}/tests/ 11 | _PYTHON_PATHS = {env:_PYTHON_SOURCE_PATHS} {env:_PYTHON_TEST_PATHS} 12 | _YAPF_OPTIONS = --style={toxinidir}/style.yapf --recursive {env:_PYTHON_PATHS} 13 | _ISORT_OPTIONS = --settings-path={toxinidir}/.isort.cfg --recursive {env:_PYTHON_PATHS} 14 | COVERAGE_FILE={toxinidir}/.coverage 15 | commands = 16 | coverage run --source lddwrap -m unittest discover --top-level-directory {toxinidir} {toxinidir}/tests 17 | coverage xml -o {toxinidir}/coverage.xml 18 | coverage report 19 | 20 | [testenv:pex] 21 | deps = 22 | pex==1.5.1 23 | commands = 24 | pex -r {toxinidir}/requirements.txt -v -e lddwrap.main:main -o {toxworkdir}/{envname}/pylddwrap.pex 25 | 26 | [testenv:format] 27 | commands = 28 | yapf --in-place {env:_YAPF_OPTIONS} 29 | isort {env:_ISORT_OPTIONS} 30 | 31 | [testenv:check] 32 | allowlist_externals = 33 | find 34 | commands = 35 | yapf --diff {env:_YAPF_OPTIONS} 36 | mypy --config-file {toxinidir}/mypy.ini {env:_PYTHON_PATHS} 37 | isort --check-only {env:_ISORT_OPTIONS} 38 | pylint --rcfile={toxinidir}/pylint.rc {env:_PYTHON_PATHS} 39 | pydocstyle {env:_PYTHON_SOURCE_PATHS} 40 | find {toxinidir} -regextype posix-egrep -regex '{toxinidir}/(lddwrap/.*\.py|README.rst)' -exec {envpython} -m doctest \{\} ; 41 | pyicontract-lint {toxinidir}/lddwrap/ 42 | python {toxinidir}/setup.py check --restructuredtext --strict 43 | 44 | [testenv:check-coverage] 45 | changedir = {toxinidir} 46 | commands = 47 | coverage combine coverage_reports/ 48 | coverage xml -o {toxinidir}/coverage.xml 49 | coverage report --fail-under=90 --ignore-errors --show-missing 50 | diff-cover --fail-under=100 {posargs:--compare-branch=master} coverage.xml 51 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Test lddwrap.""" 2 | 3 | import os 4 | import shlex 5 | import tempfile 6 | import textwrap 7 | from typing import Optional 8 | 9 | 10 | class MockLdd: 11 | """Manage context for a mock ldd script.""" 12 | 13 | def __init__(self, out: str, out_unused: str) -> None: 14 | self._tmpdir = None # type: Optional[tempfile.TemporaryDirectory] 15 | self.out = out 16 | self.out_unused = out_unused 17 | self._old_path = None # type: Optional[str] 18 | 19 | def __enter__(self) -> None: 20 | self._tmpdir = tempfile.TemporaryDirectory() 21 | 22 | # Create a mock bach script called ``ldd`` based on 23 | # https://github.com/lattera/glibc/blob/master/elf/ldd.bash.in 24 | script = textwrap.dedent('''\ 25 | #!/usr/bin/env bash 26 | if [ $# -eq 1 ]; then 27 | echo {out} 28 | elif [ "$1" = "--unused" ]; then 29 | echo {out_unused} 30 | 31 | # Return code 1 implies at least one unused dependency. 32 | exit 1 33 | else 34 | >&2 echo '$0 is: ' $0 35 | >&2 echo '$1 is: ' $1 36 | >&2 echo '$2 is: ' $2 37 | >&2 echo "Unhandled command line arguments (count: $#): $@" 38 | exit 1984 39 | fi 40 | '''.format( 41 | out=shlex.quote(self.out), out_unused=shlex.quote(self.out_unused))) 42 | 43 | pth = os.path.join(self._tmpdir.name, 'ldd') 44 | with open(pth, 'wt') as fid: 45 | fid.write(script) 46 | 47 | os.chmod(pth, 0o700) 48 | 49 | self._old_path = os.environ.get('PATH', None) 50 | 51 | os.environ['PATH'] = (self._tmpdir.name if self._old_path is None else 52 | self._tmpdir.name + os.pathsep + self._old_path) 53 | 54 | def __exit__(self, exc_type, exc_val, exc_tb): 55 | self._tmpdir.cleanup() 56 | if self._old_path is None: 57 | del os.environ['PATH'] 58 | else: 59 | os.environ['PATH'] = self._old_path 60 | -------------------------------------------------------------------------------- /precommit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Runs precommit checks on the repository.""" 3 | import argparse 4 | import os 5 | import pathlib 6 | import shutil 7 | import subprocess 8 | import sys 9 | import sysconfig 10 | import textwrap 11 | 12 | # os.fspath() is 3.6+. str is a reasonable substitute 13 | # https://docs.python.org/3.9/library/os.html#os.fspath 14 | fspath = getattr(os, "fspath", str) 15 | 16 | 17 | class ToxNotFoundError(Exception): 18 | """Rasie when tox not found in expected locations.""" 19 | 20 | 21 | def main() -> int: 22 | """Execute the main routine.""" 23 | parser = argparse.ArgumentParser() 24 | parser.add_argument( 25 | "--overwrite", 26 | help= 27 | "Overwrites the unformatted source files with the well-formatted code in place. " # pylint: disable=line-too-long 28 | "If not set, an exception is raised if any of the files do not conform to the style guide.", # pylint: disable=line-too-long 29 | action='store_true') 30 | 31 | args = parser.parse_args() 32 | 33 | tox_environments = ["check", "_auto_version"] 34 | 35 | if args.overwrite: 36 | tox_environments.insert(0, "format") 37 | 38 | tox_from_path = shutil.which("tox") 39 | 40 | maybe_scripts = sysconfig.get_path("scripts") 41 | tox_from_scripts = pathlib.Path(maybe_scripts).joinpath( 42 | "tox") if maybe_scripts is not None else None 43 | 44 | if tox_from_path is not None: 45 | tox = pathlib.Path(tox_from_path) 46 | elif tox_from_scripts is not None and tox_from_scripts.is_file(): 47 | tox = tox_from_scripts 48 | else: 49 | message = textwrap.dedent(f'''\ 50 | 'tox' executable not found in PATH or scripts directory 51 | PATH: {os.environ['PATH']} 52 | scripts: {maybe_scripts} 53 | ''') 54 | raise ToxNotFoundError(message) 55 | 56 | repo_root = pathlib.Path(__file__).parent 57 | 58 | tox_ini = repo_root.joinpath("tox.ini") 59 | 60 | command = [ 61 | fspath(tox), "-c", 62 | fspath(tox_ini), "-e", ",".join(tox_environments) 63 | ] 64 | completed_process = subprocess.run(command) # pylint: disable=W1510 65 | 66 | return completed_process.returncode 67 | 68 | 69 | if __name__ == "__main__": 70 | sys.exit(main()) 71 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """A setuptools based setup module. 2 | 3 | See: 4 | https://packaging.python.org/en/latest/distributing.html 5 | https://github.com/pypa/sampleproject 6 | """ 7 | import os 8 | 9 | import pylddwrap_meta 10 | # pylint: disable=wrong-import-order 11 | from setuptools import find_packages, setup 12 | 13 | # pylint: disable=redefined-builtin 14 | 15 | here = os.path.abspath(os.path.dirname(__file__)) # pylint: disable=invalid-name 16 | 17 | with open(os.path.join(here, 'README.rst'), encoding='utf-8') as fid: 18 | long_description = fid.read().strip() # pylint: disable=invalid-name 19 | 20 | with open(os.path.join(here, 'requirements.txt'), encoding='utf-8') as fid: 21 | install_requires = [ 22 | line for line in fid.read().splitlines() if line.strip() 23 | ] 24 | 25 | setup( 26 | name=pylddwrap_meta.__title__, 27 | version=pylddwrap_meta.__version__, 28 | description=pylddwrap_meta.__description__, 29 | long_description=long_description, 30 | url=pylddwrap_meta.__url__, 31 | author=pylddwrap_meta.__author__, 32 | author_email=pylddwrap_meta.__author_email__, 33 | # yapf: disable 34 | classifiers=[ 35 | 'Development Status :: 5 - Production/Stable', 36 | 'Intended Audience :: Developers', 37 | 'License :: OSI Approved :: MIT License', 38 | 'Programming Language :: Python :: Implementation :: CPython', 39 | 'Programming Language :: Python :: Implementation :: PyPy', 40 | 'Programming Language :: Python :: 3.6', 41 | 'Programming Language :: Python :: 3.7', 42 | 'Programming Language :: Python :: 3.8', 43 | 'Programming Language :: Python :: 3.9', 44 | ], 45 | # yapf: enable 46 | license='License :: OSI Approved :: MIT License', 47 | keywords='ldd dependency dependencies lddwrap pylddwrap', 48 | packages=find_packages(exclude=['tests']), 49 | python_requires='>=3.6', 50 | install_requires=install_requires, 51 | extras_require={ 52 | 'dev': [ 53 | # yapf: disable 54 | 'mypy==0.790; implementation_name != "pypy"', 55 | 'pylint==2.7.1', 56 | 'yapf==0.24.0', 57 | 'tox>=3.0.0', 58 | 'coverage>=5.5.0,<6', 59 | 'diff-cover>=5.0.1,<6; python_version >= "3.6"', 60 | 'isort<5', 61 | 'pydocstyle>=3.0.0,<4', 62 | 'pyicontract-lint>=2.0.0,<3', 63 | 'docutils>=0.14,<1', 64 | 'pygments>=2.2.0,<3' 65 | # yapf: enable 66 | ] 67 | }, 68 | scripts=['bin/pylddwrap'], 69 | py_modules=['lddwrap', 'pylddwrap_meta'], 70 | package_data={"lddwrap": ["py.typed"]}, 71 | data_files=[('.', ['LICENSE', 'README.rst', 'requirements.txt'])]) 72 | -------------------------------------------------------------------------------- /lddwrap/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Determine shared libraries required by a program.""" 3 | 4 | # This file is necessary so that we can specify the entry point for pex. 5 | 6 | import argparse 7 | import pathlib 8 | import sys 9 | from typing import Any, List, TextIO 10 | 11 | import lddwrap 12 | import pylddwrap_meta 13 | 14 | 15 | class Args: 16 | """Represent parsed command-line arguments.""" 17 | 18 | def __init__(self, args: Any) -> None: 19 | """Initialize with arguments parsed with ``argparse``.""" 20 | self.format = str(args.format) 21 | self.path = pathlib.Path(args.path) 22 | self.sort_by = (None if args.sort_by is None else str(args.sort_by)) 23 | 24 | 25 | def parse_args(sys_argv: List[str]) -> Args: 26 | """Parse command-line arguments.""" 27 | parser = argparse.ArgumentParser(description=__doc__) 28 | parser.add_argument( 29 | "-v", 30 | "--version", 31 | help="Display the version and return immediately", 32 | action='version', 33 | version=pylddwrap_meta.__version__ + "\n") 34 | parser.add_argument( 35 | "-f", 36 | "--format", 37 | help="Specify the output format.", 38 | default='verbose', 39 | choices=['verbose', 'json']) 40 | parser.add_argument( 41 | '-s', 42 | '--sorted', 43 | # ``sorted`` is reserved for a built-in method, so we need to pick 44 | # a different identifier. 45 | dest='sort_by', 46 | help='If set, the output is sorted by the given attribute', 47 | const='soname', 48 | choices=lddwrap.DEPENDENCY_ATTRIBUTES, 49 | nargs='?') 50 | 51 | parser.add_argument("path", help="Specify path to the binary") 52 | 53 | args = parser.parse_args(sys_argv[1:]) 54 | 55 | if pathlib.Path(args.path).is_dir(): 56 | parser.error("Path '{}' is a dir. Path to file required. Check out " 57 | "--help for more information.".format(args.path)) 58 | 59 | if not pathlib.Path(args.path).is_file(): 60 | parser.error( 61 | "Path '{}' is not a file. Path to file required. Check out " 62 | "--help for more information.".format(args.path)) 63 | 64 | return Args(args=args) 65 | 66 | 67 | def _main(args: Args, stream: TextIO) -> int: 68 | """Execute the main routine.""" 69 | # pylint: disable=protected-access 70 | deps = lddwrap.list_dependencies(path=args.path, unused=True) 71 | 72 | if args.sort_by is not None: 73 | lddwrap._sort_dependencies_in_place(deps=deps, sort_by=args.sort_by) 74 | 75 | if args.format == 'verbose': 76 | lddwrap._output_verbose(deps=deps, stream=stream) 77 | elif args.format == 'json': 78 | lddwrap._output_json(deps=deps, stream=stream) 79 | else: 80 | raise NotImplementedError("Unhandled format: {}".format(args.format)) 81 | stream.write('\n') 82 | 83 | return 0 84 | 85 | 86 | def main() -> None: 87 | """Wrap the main routine so that it can be tested.""" 88 | args = parse_args(sys_argv=sys.argv) 89 | sys.exit(_main(args=args, stream=sys.stdout)) 90 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | sys.path.insert(0, os.path.abspath('../..')) 18 | 19 | import pylddwrap_meta 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = pylddwrap_meta.__title__ 24 | copyright = pylddwrap_meta.__copyright__ 25 | author = pylddwrap_meta.__author__ 26 | description = pylddwrap_meta.__description__ 27 | 28 | # The short X.Y version 29 | version = '' 30 | # The full version, including alpha/beta/rc tags 31 | release = pylddwrap_meta.__version__ 32 | 33 | 34 | # -- General configuration --------------------------------------------------- 35 | 36 | # If your documentation needs a minimal Sphinx version, state it here. 37 | # 38 | # needs_sphinx = '1.0' 39 | 40 | # Add any Sphinx extension module names here, as strings. They can be 41 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 42 | # ones. 43 | extensions = [ 44 | 'sphinx.ext.autodoc', 45 | 'sphinx.ext.doctest', 46 | 'sphinx_autodoc_typehints', 47 | 'sphinx_icontract' 48 | ] 49 | 50 | # Add any paths that contain templates here, relative to this directory. 51 | templates_path = ['_templates'] 52 | 53 | # The suffix(es) of source filenames. 54 | # You can specify multiple suffix as a list of string: 55 | # 56 | # source_suffix = ['.rst', '.md'] 57 | source_suffix = '.rst' 58 | 59 | # The master toctree document. 60 | master_doc = 'index' 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | # 65 | # This is also used if you do content translation via gettext catalogs. 66 | # Usually you set "language" from the command line for these cases. 67 | language = None 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | # This pattern also affects html_static_path and html_extra_path. 72 | exclude_patterns = [] 73 | 74 | # The name of the Pygments (syntax highlighting) style to use. 75 | pygments_style = 'sphinx' 76 | 77 | 78 | # -- Options for HTML output ------------------------------------------------- 79 | 80 | # The theme to use for HTML and HTML Help pages. See the documentation for 81 | # a list of builtin themes. 82 | # 83 | html_theme = 'sphinx_rtd_theme' 84 | 85 | # Theme options are theme-specific and customize the look and feel of a theme 86 | # further. For a list of options available for each theme, see the 87 | # documentation. 88 | # 89 | # html_theme_options = {} 90 | 91 | # Add any paths that contain custom static files (such as style sheets) here, 92 | # relative to this directory. They are copied after the builtin static files, 93 | # so a file named "default.css" will overwrite the builtin "default.css". 94 | html_static_path = ['_static'] 95 | 96 | # Custom sidebar templates, must be a dictionary that maps document names 97 | # to template names. 98 | # 99 | # The default sidebars (for documents that don't match any pattern) are 100 | # defined by theme itself. Builtin themes are using these templates by 101 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 102 | # 'searchbox.html']``. 103 | # 104 | # html_sidebars = {} 105 | 106 | 107 | # -- Options for HTMLHelp output --------------------------------------------- 108 | 109 | # Output file base name for HTML help builder. 110 | htmlhelp_basename = 'lddwrapdoc' 111 | 112 | 113 | # -- Options for LaTeX output ------------------------------------------------ 114 | 115 | latex_elements = { 116 | # The paper size ('letterpaper' or 'a4paper'). 117 | # 118 | # 'papersize': 'letterpaper', 119 | 120 | # The font size ('10pt', '11pt' or '12pt'). 121 | # 122 | # 'pointsize': '10pt', 123 | 124 | # Additional stuff for the LaTeX preamble. 125 | # 126 | # 'preamble': '', 127 | 128 | # Latex figure (float) alignment 129 | # 130 | # 'figure_align': 'htbp', 131 | } 132 | 133 | # Grouping the document tree into LaTeX files. List of tuples 134 | # (source start file, target name, title, 135 | # author, documentclass [howto, manual, or own class]). 136 | latex_documents = [ 137 | (master_doc, 138 | '{}.tex'.format(project), 139 | '{} Documentation'.format(project), 140 | author, 141 | 'manual'), 142 | ] 143 | 144 | 145 | # -- Options for manual page output ------------------------------------------ 146 | 147 | # One entry per manual page. List of tuples 148 | # (source start file, name, description, authors, manual section). 149 | man_pages = [ 150 | (master_doc, 151 | project, 152 | '{} Documentation'.format(project), 153 | [author], 154 | 1) 155 | ] 156 | 157 | 158 | 159 | # -- Options for Texinfo output ---------------------------------------------- 160 | 161 | # Grouping the document tree into Texinfo files. List of tuples 162 | # (source start file, target name, title, author, 163 | # dir menu entry, description, category) 164 | texinfo_documents = [ 165 | (master_doc, project, '{} Documentation'.format(project), 166 | author, project, description, 167 | 'Miscellaneous'), 168 | ] 169 | 170 | 171 | # -- Options for Epub output ------------------------------------------------- 172 | 173 | # Bibliographic Dublin Core info. 174 | epub_title = project 175 | 176 | # The unique identifier of the text. This can be a ISBN number 177 | # or the project homepage. 178 | # 179 | # epub_identifier = '' 180 | 181 | # A unique identification for the text. 182 | # 183 | # epub_uid = '' 184 | 185 | # A list of files that should not be packed into the epub file. 186 | epub_exclude_files = ['search.html'] 187 | 188 | 189 | # -- Extension configuration ------------------------------------------------- -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | tags: [ "**" ] 7 | pull_request: 8 | branches: [ "**" ] 9 | 10 | defaults: 11 | run: 12 | shell: bash 13 | 14 | jobs: 15 | build: 16 | name: ${{ matrix.task.name}} - ${{ matrix.os.name }} ${{ matrix.python.name }} 17 | runs-on: ${{ matrix.os.runs-on }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | os: 22 | - name: Linux 23 | runs-on: ubuntu-latest 24 | python: 25 | - name: CPython 3.8 26 | tox: py38 27 | action: 3.8 28 | task: 29 | - name: Build 30 | tox: build 31 | coverage: false 32 | 33 | steps: 34 | - uses: actions/checkout@v2 35 | 36 | - name: Set up ${{ matrix.python.name }} 37 | uses: actions/setup-python@v2 38 | with: 39 | python-version: ${{ matrix.python.action }} 40 | 41 | - name: Install dependencies 42 | run: | 43 | python -m pip install --upgrade pip setuptools wheel 44 | python -m pip install build check-manifest twine 45 | 46 | - uses: twisted/python-info-action@v1 47 | 48 | - name: Build 49 | run: | 50 | check-manifest --verbose . 51 | 52 | python -m build --sdist --outdir dist/ . 53 | 54 | mkdir empty/ 55 | cd empty 56 | 57 | tar -xvf ../dist/* 58 | cd * 59 | 60 | # build the wheel from the sdist 61 | python -m build --wheel --outdir ../../dist/ . 62 | cd ../../ 63 | 64 | twine check dist/* 65 | 66 | - name: Publish 67 | uses: actions/upload-artifact@v2 68 | with: 69 | name: dist 70 | path: dist/ 71 | 72 | test: 73 | # Should match JOB_NAME below 74 | name: ${{ matrix.task.name}} - ${{ matrix.os.name }} ${{ matrix.python.name }} 75 | runs-on: ${{ matrix.os.runs-on }} 76 | needs: 77 | - build 78 | strategy: 79 | fail-fast: false 80 | matrix: 81 | os: 82 | - name: Linux 83 | runs-on: ubuntu-latest 84 | python: 85 | - name: CPython 3.6 86 | tox: py36 87 | action: 3.6 88 | - name: CPython 3.7 89 | tox: py37 90 | action: 3.7 91 | - name: CPython 3.8 92 | tox: py38 93 | action: 3.8 94 | - name: CPython 3.9 95 | tox: py39 96 | action: 3.9 97 | - name: PyPy 3.6 98 | tox: pypy36 99 | action: pypy-3.6 100 | - name: PyPy 3.7 101 | tox: pypy37 102 | action: pypy-3.7 103 | task: 104 | - name: Test 105 | coverage: true 106 | include: 107 | - os: 108 | name: Linux 109 | runs-on: ubuntu-latest 110 | python: 111 | name: CPython 3.8 112 | action: 3.8 113 | task: 114 | name: Check 115 | tox: check 116 | coverage: false 117 | 118 | env: 119 | # Should match name above 120 | JOB_NAME: ${{ matrix.task.name}} - ${{ matrix.os.name }} ${{ matrix.python.name }} 121 | TOXENV: ${{ matrix.task.tox }}${{ fromJSON('["", "-"]')[matrix.task.tox != null && matrix.python.tox != null] }}${{ matrix.python.tox }} 122 | 123 | steps: 124 | - uses: actions/checkout@v2 125 | 126 | - name: Download package files 127 | uses: actions/download-artifact@v2 128 | with: 129 | name: dist 130 | path: dist/ 131 | 132 | - name: Set up ${{ matrix.python.name }} 133 | uses: actions/setup-python@v2 134 | with: 135 | python-version: ${{ matrix.python.action }} 136 | 137 | - name: Install dependencies 138 | run: | 139 | python -m pip install --upgrade pip setuptools wheel 140 | pip install tox 141 | 142 | - name: Prepare tox environment 143 | run: | 144 | tox --notest --installpkg dist/*.whl 145 | 146 | - name: Runner info 147 | uses: twisted/python-info-action@v1 148 | 149 | - name: Tox info 150 | uses: twisted/python-info-action@v1 151 | with: 152 | python-path: .tox/${{ env.TOXENV }}/*/python 153 | 154 | - name: Run tox environment 155 | run: | 156 | tox --skip-pkg-install 157 | 158 | - name: Coverage Processing 159 | if: matrix.task.coverage 160 | run: | 161 | mkdir coverage_reports 162 | cp .coverage "coverage_reports/.coverage.${{ env.JOB_NAME }}" 163 | cp coverage.xml "coverage_reports/coverage.${{ env.JOB_NAME }}.xml" 164 | 165 | - name: Upload Coverage 166 | if: matrix.task.coverage 167 | uses: actions/upload-artifact@v2 168 | with: 169 | name: coverage 170 | path: coverage_reports/* 171 | 172 | coverage: 173 | name: ${{ matrix.task.name}} - ${{ matrix.os.name }} ${{ matrix.python.name }} 174 | runs-on: ${{ matrix.os.runs-on }} 175 | needs: 176 | - test 177 | strategy: 178 | fail-fast: false 179 | matrix: 180 | include: 181 | - os: 182 | name: Linux 183 | runs-on: ubuntu-latest 184 | python: 185 | name: CPython 3.8 186 | action: 3.8 187 | task: 188 | name: Coverage 189 | tox: check-coverage 190 | coverage: false 191 | download_coverage: true 192 | 193 | env: 194 | TOXENV: ${{ matrix.task.tox }}${{ fromJSON('["", "-"]')[matrix.task.tox != null && matrix.python.tox != null] }}${{ matrix.python.tox }} 195 | 196 | steps: 197 | - uses: actions/checkout@v2 198 | with: 199 | fetch-depth: 0 200 | ref: ${{ github.event.pull_request.head.sha }} 201 | 202 | - name: Download package files 203 | uses: actions/download-artifact@v2 204 | with: 205 | name: dist 206 | path: dist/ 207 | 208 | - name: Download Coverage 209 | if: matrix.task.download_coverage 210 | uses: actions/download-artifact@v2 211 | with: 212 | name: coverage 213 | path: coverage_reports 214 | 215 | - name: Set up ${{ matrix.python.name }} 216 | uses: actions/setup-python@v2 217 | with: 218 | python-version: ${{ matrix.python.action }} 219 | 220 | - name: Install dependencies 221 | run: | 222 | python -m pip install --upgrade pip setuptools wheel 223 | pip install tox coveralls 224 | 225 | - name: Prepare tox environment 226 | run: | 227 | tox --notest --installpkg dist/*.whl 228 | 229 | - name: Runner info 230 | uses: twisted/python-info-action@v1 231 | 232 | - name: Tox info 233 | uses: twisted/python-info-action@v1 234 | with: 235 | python-path: .tox/${{ env.TOXENV }}/*/python 236 | 237 | - name: Run tox environment 238 | env: 239 | BASE_REF: ${{ fromJSON(format('[{0}, {1}]', toJSON(github.event.before), toJSON(format('origin/{0}', github.base_ref))))[github.base_ref != ''] }} 240 | run: | 241 | tox --skip-pkg-install -- --compare-branch="${BASE_REF}" 242 | 243 | - name: Coverage Processing 244 | if: always() 245 | run: | 246 | mkdir all_coverage_report 247 | cp .coverage "all_coverage_report/.coverage.all" 248 | cp coverage.xml "all_coverage_report/coverage.all.xml" 249 | 250 | - name: Upload Coverage 251 | if: always() 252 | uses: actions/upload-artifact@v2 253 | with: 254 | name: coverage 255 | path: all_coverage_report/* 256 | 257 | - name: Publish to Coveralls 258 | continue-on-error: true 259 | env: 260 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 261 | if: always() 262 | run: | 263 | python -m coveralls -v 264 | 265 | all: 266 | name: All 267 | runs-on: ubuntu-latest 268 | needs: 269 | - build 270 | - coverage 271 | - test 272 | steps: 273 | - name: This 274 | shell: python 275 | run: import this 276 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Test the main routine.""" 3 | 4 | import io 5 | import json 6 | import pathlib 7 | import shutil 8 | import textwrap 9 | import unittest 10 | from typing import List, TextIO, cast 11 | 12 | import lddwrap 13 | import lddwrap.main 14 | import pylddwrap_meta 15 | 16 | # pylint: disable=missing-docstring 17 | import tests 18 | 19 | LS = shutil.which("ls") or "/bin/ls" 20 | PWD = shutil.which("pwd") or "/bin/pwd" 21 | 22 | 23 | class TestParseArgs(unittest.TestCase): 24 | def test_single_path(self): 25 | args = lddwrap.main.parse_args(sys_argv=['some-executable.py', LS]) 26 | self.assertEqual(pathlib.Path(LS), args.path) 27 | 28 | def test_format(self): 29 | args = lddwrap.main.parse_args( 30 | sys_argv=['some-executable.py', LS, "--format", "json"]) 31 | self.assertEqual("json", args.format) 32 | 33 | 34 | class TestMain(unittest.TestCase): 35 | # pylint: disable=protected-access 36 | def test_verbose(self): 37 | deps = [] # type: List[lddwrap.Dependency] 38 | for index in range(3): 39 | deps.append( 40 | lddwrap.Dependency( 41 | found=True, 42 | soname='lib{}.so'.format(index), 43 | path=pathlib.Path('/bin/lib{}.so'.format(index)), 44 | mem_address=hex(index), 45 | unused=False)) 46 | 47 | buf = io.StringIO() 48 | stream = cast(TextIO, buf) 49 | lddwrap._output_verbose(deps=deps, stream=stream) 50 | # pylint: disable=trailing-whitespace 51 | expected_output = textwrap.dedent("""\ 52 | soname | path | found | mem_address | unused 53 | --------+--------------+-------+-------------+------- 54 | lib0.so | /bin/lib0.so | True | 0x0 | False 55 | lib1.so | /bin/lib1.so | True | 0x1 | False 56 | lib2.so | /bin/lib2.so | True | 0x2 | False """) 57 | 58 | output = textwrap.dedent(buf.getvalue()) 59 | self.assertEqual(expected_output, output) 60 | 61 | def test_json(self): 62 | deps = [] # type: List[lddwrap.Dependency] 63 | for index in range(2): 64 | deps.append( 65 | lddwrap.Dependency( 66 | found=True, 67 | soname='lib{}.so'.format(index), 68 | path=pathlib.Path('/bin/lib{}.so'.format(index)), 69 | mem_address=hex(index), 70 | unused=False)) 71 | 72 | buf = io.StringIO() 73 | stream = cast(TextIO, buf) 74 | lddwrap._output_json(deps=deps, stream=stream) 75 | 76 | expected_output = textwrap.dedent("""\ 77 | [ 78 | { 79 | "soname": "lib0.so", 80 | "path": "/bin/lib0.so", 81 | "found": true, 82 | "mem_address": "0x0", 83 | "unused": false 84 | }, 85 | { 86 | "soname": "lib1.so", 87 | "path": "/bin/lib1.so", 88 | "found": true, 89 | "mem_address": "0x1", 90 | "unused": false 91 | } 92 | ]""") 93 | 94 | output = textwrap.dedent(buf.getvalue()) 95 | self.assertEqual(expected_output, output) 96 | 97 | def test_json_format(self): 98 | deps = [] # type: List[lddwrap.Dependency] 99 | for index in range(2): 100 | deps.append( 101 | lddwrap.Dependency( 102 | found=True, 103 | soname='lib{}.so'.format(index), 104 | path=pathlib.Path('/bin/lib{}.so'.format(index)), 105 | mem_address=hex(index), 106 | unused=False)) 107 | 108 | buf = io.StringIO() 109 | stream = cast(TextIO, buf) 110 | 111 | json.dump(obj=[dep.as_mapping() for dep in deps], fp=stream, indent=2) 112 | try: 113 | json.loads(buf.getvalue()) 114 | except ValueError: 115 | self.fail("The following data was not in json format\n\n{}".format( 116 | buf.getvalue())) 117 | 118 | def test_main(self): 119 | buf = io.StringIO() 120 | stream = cast(TextIO, buf) 121 | 122 | args = lddwrap.main.parse_args(sys_argv=["some-executable.py", PWD]) 123 | 124 | with tests.MockLdd( 125 | out="\n".join([ 126 | "\tlinux-vdso.so.1 (0x00007ffe0953f000)", 127 | "\tlibc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd548353000)", # pylint: disable=C0301 128 | "\t/lib64/ld-linux-x86-64.so.2 (0x00007fd54894d000)", 129 | "", 130 | ]), 131 | out_unused=''): 132 | 133 | retcode = lddwrap.main._main(args=args, stream=stream) 134 | 135 | self.assertEqual(0, retcode) 136 | # pylint: disable=trailing-whitespace 137 | expected_output = textwrap.dedent("""\ 138 | soname | path | found | mem_address | unused 139 | ----------------+---------------------------------+-------+--------------------+------- 140 | linux-vdso.so.1 | None | True | 0x00007ffe0953f000 | False 141 | libc.so.6 | /lib/x86_64-linux-gnu/libc.so.6 | True | 0x00007fd548353000 | False 142 | None | /lib64/ld-linux-x86-64.so.2 | True | 0x00007fd54894d000 | False 143 | """) 144 | output = textwrap.dedent(buf.getvalue()) 145 | 146 | self.assertEqual(expected_output, output) 147 | 148 | def test_sorted_without_specific_attribute(self): 149 | buf = io.StringIO() 150 | stream = cast(TextIO, buf) 151 | 152 | args = lddwrap.main.parse_args( 153 | sys_argv=["some-executable.py", PWD, "--sorted"]) 154 | 155 | with tests.MockLdd( 156 | out="\n".join([ 157 | "\tlinux-vdso.so.1 (0x00007ffe0953f000)", 158 | "\tlibc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd548353000)", # pylint: disable=C0301 159 | "\t/lib64/ld-linux-x86-64.so.2 (0x00007fd54894d000)", 160 | "", 161 | ]), 162 | out_unused=''): 163 | 164 | retcode = lddwrap.main._main(args=args, stream=stream) 165 | 166 | self.assertEqual(0, retcode) 167 | # pylint: disable=trailing-whitespace 168 | expected_output = textwrap.dedent("""\ 169 | soname | path | found | mem_address | unused 170 | ----------------+---------------------------------+-------+--------------------+------- 171 | None | /lib64/ld-linux-x86-64.so.2 | True | 0x00007fd54894d000 | False 172 | libc.so.6 | /lib/x86_64-linux-gnu/libc.so.6 | True | 0x00007fd548353000 | False 173 | linux-vdso.so.1 | None | True | 0x00007ffe0953f000 | False 174 | """) 175 | output = textwrap.dedent(buf.getvalue()) 176 | 177 | self.assertEqual(expected_output, output) 178 | 179 | def test_sorted_with_specific_attribute(self): 180 | buf = io.StringIO() 181 | stream = cast(TextIO, buf) 182 | 183 | args = lddwrap.main.parse_args( 184 | sys_argv=["some-executable.py", PWD, "--sorted", "path"]) 185 | 186 | with tests.MockLdd( 187 | out="\n".join([ 188 | "\tlinux-vdso.so.1 (0x00007ffe0953f000)", 189 | "\tlibc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd548353000)", # pylint: disable=C0301 190 | "\t/lib64/ld-linux-x86-64.so.2 (0x00007fd54894d000)", 191 | ]), 192 | out_unused=''): 193 | 194 | retcode = lddwrap.main._main(args=args, stream=stream) 195 | 196 | self.assertEqual(0, retcode) 197 | # pylint: disable=trailing-whitespace 198 | expected_output = textwrap.dedent("""\ 199 | soname | path | found | mem_address | unused 200 | ----------------+---------------------------------+-------+--------------------+------- 201 | linux-vdso.so.1 | None | True | 0x00007ffe0953f000 | False 202 | libc.so.6 | /lib/x86_64-linux-gnu/libc.so.6 | True | 0x00007fd548353000 | False 203 | None | /lib64/ld-linux-x86-64.so.2 | True | 0x00007fd54894d000 | False 204 | """) 205 | output = textwrap.dedent(buf.getvalue()) 206 | 207 | self.assertEqual(expected_output, output) 208 | 209 | def test_version(self): 210 | with self.assertRaises(SystemExit): 211 | buf = io.StringIO() 212 | stream = cast(TextIO, buf) 213 | args = lddwrap.main.parse_args( 214 | sys_argv=["some-executable.py", "--version"]) 215 | 216 | retcode = lddwrap.main._main(args=args, stream=stream) 217 | self.assertEqual(0, retcode) 218 | self.assertEqual('{}\n'.format(pylddwrap_meta.__version__), 219 | buf.getvalue()) 220 | 221 | 222 | if __name__ == '__main__': 223 | unittest.main() 224 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pylddwrap 2 | ========= 3 | .. image:: https://github.com/Parquery/pylddwrap/actions/workflows/ci.yml/badge.svg?branch=master 4 | :target: https://github.com/Parquery/pylddwrap/actions/workflows/ci.yml?query=branch%3Amaster 5 | :alt: Build Status 6 | 7 | .. image:: https://coveralls.io/repos/github/Parquery/pylddwrap/badge.svg?branch=master 8 | :target: https://coveralls.io/github/Parquery/pylddwrap?branch=master 9 | :alt: Coverage 10 | 11 | .. image:: https://badges.frapsoft.com/os/mit/mit.png?v=103 12 | :target: https://opensource.org/licenses/mit-license.php 13 | :alt: MIT License 14 | 15 | .. image:: https://badge.fury.io/py/pylddwrap.svg 16 | :target: https://badge.fury.io/py/pylddwrap 17 | :alt: PyPI - version 18 | 19 | .. image:: https://img.shields.io/pypi/pyversions/pylddwrap.svg 20 | :alt: PyPI - Python Version 21 | 22 | .. image:: https://readthedocs.org/projects/pylddwrap/badge/?version=latest 23 | :target: https://pylddwrap.readthedocs.io/en/latest/?badge=latest 24 | :alt: Documentation Status 25 | 26 | Pylddwrap wraps ldd \*nix utility to determine shared libraries required by a program. 27 | 28 | We need to dynamically package subset of our system at deployment time. Consequently, we have to determine the 29 | dependencies on shared libraries of our binaries programmatically. 30 | 31 | The output of ldd Linux command, while informative, is not structured enough to be easily integrated into a program. 32 | At the time of this writing, we only found two alternative ldd wrappers on Internet 33 | `python-ldd `_ and `ldd.py `_, but their 34 | output was either too basic for our use case or the project was still incipient. 35 | 36 | Pylddwrap, in contrast, returns a well-structured list of the dependencies. The command-line tool outputs the 37 | dependencies either as a table (for visual inspection) or as a JSON-formatted string (for use with other tools). 38 | The included Python module lddwrap returns a Python object with type annotations so that it can be used readily by the 39 | deployment scripts and other modules. 40 | 41 | For more information on the ldd tool, please see `ldd manual `_. 42 | 43 | Usage 44 | ===== 45 | 46 | Command-Line Tool pylddwrap 47 | --------------------------- 48 | 49 | * Assume we need the dependencies of the /bin/ls. The following command gives them as a table: 50 | 51 | .. code-block:: bash 52 | 53 | pylddwrap /bin/ls 54 | 55 | * The output of the command looks like this: 56 | 57 | .. code-block:: text 58 | 59 | soname | path | found | mem_address | unused 60 | ----------------+---------------------------------------+-------+--------------------+------- 61 | linux-vdso.so.1 | None | True | 0x00007ffd8750f000 | False 62 | libselinux.so.1 | /lib/x86_64-linux-gnu/libselinux.so.1 | True | 0x00007f4e73dc3000 | True 63 | libc.so.6 | /lib/x86_64-linux-gnu/libc.so.6 | True | 0x00007f4e739f9000 | False 64 | libpcre.so.3 | /lib/x86_64-linux-gnu/libpcre.so.3 | True | 0x00007f4e73789000 | False 65 | libdl.so.2 | /lib/x86_64-linux-gnu/libdl.so.2 | True | 0x00007f4e73585000 | False 66 | None | /lib64/ld-linux-x86-64.so.2 | True | 0x00007f4e73fe5000 | False 67 | libpthread.so.0 | /lib/x86_64-linux-gnu/libpthread.so.0 | True | 0x00007f4e73368000 | False 68 | 69 | 70 | * To obtain the dependencies as JSON, invoke: 71 | 72 | .. code-block:: bash 73 | 74 | pylddwrap --format json /bin/ls 75 | 76 | * The JSON output is structured like this: 77 | 78 | .. code-block:: text 79 | 80 | [ 81 | { 82 | "soname": "linux-vdso.so.1", 83 | "path": "None", 84 | "found": true, 85 | "mem_address": "0x00007ffed857f000", 86 | "unused": false 87 | }, 88 | ... 89 | ] 90 | 91 | * You can also sort the table with ``--sorted`` which will sort by ``soname``: 92 | 93 | .. code-block:: bash 94 | 95 | pylddwrap /bin/pwd --sorted 96 | 97 | * Pylddwrap gives the table sorted by ``soname``: 98 | 99 | .. code-block:: text 100 | 101 | soname | path | found | mem_address | unused 102 | ----------------+---------------------------------+-------+--------------------+------- 103 | None | /lib64/ld-linux-x86-64.so.2 | True | 0x00007fd54894d000 | False 104 | libc.so.6 | /lib/x86_64-linux-gnu/libc.so.6 | True | 0x00007fd548353000 | False 105 | linux-vdso.so.1 | None | True | 0x00007ffe0953f000 | False 106 | 107 | Alternatively, you can sort by any other column. For example, to sort 108 | by ``path``: 109 | 110 | .. code-block:: bash 111 | 112 | pylddwrap /bin/pwd --sorted path 113 | 114 | * The output will be: 115 | 116 | .. code-block:: text 117 | 118 | soname | path | found | mem_address | unused 119 | ----------------+---------------------------------+-------+--------------------+------- 120 | linux-vdso.so.1 | None | True | 0x00007ffe0953f000 | False 121 | libc.so.6 | /lib/x86_64-linux-gnu/libc.so.6 | True | 0x00007fd548353000 | False 122 | None | /lib64/ld-linux-x86-64.so.2 | True | 0x00007fd54894d000 | False 123 | 124 | 125 | ldwrap Python Module 126 | -------------------- 127 | 128 | We provide lddwrap Python module which you can integrate into your deployment scripts and other modules. 129 | 130 | * The following example shows how to list the dependencies of /bin/ls: 131 | 132 | .. code-block:: python 133 | 134 | import pathlib 135 | import lddwrap 136 | 137 | path = pathlib.Path("/bin/ls") 138 | deps = lddwrap.list_dependencies(path=path) 139 | for dep in deps: 140 | print(dep) 141 | 142 | """ 143 | soname: linux-vdso.so.1, path: None, found: True, mem_address: (0x00007ffe8e2fb000), unused: None 144 | soname: libselinux.so.1, path: /lib/x86_64-linux-gnu/libselinux.so.1, found: True, mem_address: (0x00007f7759ccc000), unused: None 145 | soname: libc.so.6, path: /lib/x86_64-linux-gnu/libc.so.6, found: True, mem_address: (0x00007f7759902000), unused: None 146 | ... 147 | """ 148 | 149 | * List all dependencies of the /bin/ls utility and check if the direct dependencies are used. 150 | If unused for list_dependencies is set to False then the unused variable of the dependencies will not be determined 151 | and are therefore unknown and set to None. Otherwise information about direct usage will be retrieved and added to the 152 | dependencies. 153 | 154 | .. code-block:: python 155 | 156 | import pathlib 157 | import lddwrap 158 | 159 | path = pathlib.Path("/bin/ls") 160 | deps = lddwrap.list_dependencies(path=path, unused=True) 161 | print(deps[1]) 162 | # soname: libselinux.so.1, 163 | # path: /lib/x86_64-linux-gnu/libselinux.so.1, 164 | # found: True, 165 | # mem_address: (0x00007f5a6064a000), 166 | # unused: True 167 | 168 | * Lddwrap operates normally with the environment variables of the caller. In cases where your dependencies are 169 | determined differently than the current environment, you pass a separate environment (in form of a dictionary) as an argument: 170 | 171 | .. code-block:: python 172 | 173 | import os 174 | import pathlib 175 | import lddwrap 176 | 177 | env = os.environ.copy() 178 | env['LD_LIBRARY_PATH'] = "some/important/path" 179 | path = pathlib.Path("/bin/ls") 180 | deps = lddwrap.list_dependencies(path=path, env=env) 181 | 182 | Installation 183 | ============ 184 | 185 | * Install pylddwrap with pip: 186 | 187 | .. code-block:: bash 188 | 189 | pip3 install pylddwrap 190 | 191 | 192 | Development 193 | =========== 194 | 195 | * Check out the repository. 196 | 197 | * In the repository root, create the virtual environment: 198 | 199 | .. code-block:: bash 200 | 201 | python3 -m venv venv3 202 | 203 | * Activate the virtual environment: 204 | 205 | .. code-block:: bash 206 | 207 | source venv3/bin/activate 208 | 209 | * Install the development dependencies: 210 | 211 | .. code-block:: bash 212 | 213 | pip3 install -e .[dev] 214 | 215 | * Tests can be run directly using ``unittest``: 216 | 217 | .. code-block:: bash 218 | 219 | python3 -m unittest discover tests/ 220 | 221 | 222 | Pre-commit Checks 223 | ----------------- 224 | 225 | We provide a set of pre-commit checks that lint and check code for formatting. 226 | 227 | Namely, we use: 228 | 229 | * `yapf `_ to check the formatting. 230 | * The style of the docstrings is checked with `pydocstyle `_. 231 | * Static type analysis is performed with `mypy `_. 232 | * Various linter checks are done with `pylint `_. 233 | 234 | 235 | Apply the automatic formatting by running the ``format`` environment: 236 | 237 | .. code-block:: bash 238 | 239 | tox -e format 240 | 241 | Run the pre-commit checks and tests using ``tox``: 242 | 243 | .. code-block:: bash 244 | 245 | tox 246 | 247 | 248 | Versioning 249 | ========== 250 | We follow `Semantic Versioning `_. The version X.Y.Z indicates: 251 | 252 | * X is the major version (backward-incompatible), 253 | * Y is the minor version (backward-compatible), and 254 | * Z is the patch version (backward-compatible bug fix). 255 | -------------------------------------------------------------------------------- /lddwrap/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Wrap ldd *nix utility to determine shared libraries required by a program.""" 3 | 4 | import collections 5 | import copy 6 | import json 7 | import os 8 | import pathlib 9 | import re 10 | import subprocess 11 | # pylint: disable=unused-import 12 | from typing import Any, Dict, List, Mapping, Optional, TextIO 13 | 14 | import icontract 15 | 16 | DEPENDENCY_ATTRIBUTES = ['soname', 'path', 'found', 'mem_address', 'unused'] 17 | 18 | 19 | # yapf: disable 20 | @icontract.invariant( 21 | lambda self: not (self.soname is None and self.found) or ( 22 | self.path is not None and self.mem_address is not None) 23 | ) 24 | @icontract.invariant( 25 | lambda self: not (self.path is None and self.found) or ( 26 | self.soname is not None and self.mem_address is not None) 27 | ) 28 | @icontract.invariant( 29 | lambda self: not (self.mem_address is None and self.found) or ( 30 | self.soname is not None and self.path is not None) 31 | ) 32 | # yapf: enable 33 | class Dependency: 34 | """ 35 | Represent a shared library required by a program. 36 | 37 | :ivar found: True if ``ldd`` could resolve the library 38 | :vartype found: bool 39 | 40 | :ivar soname: library name 41 | :vartype soname: Optional[str] 42 | 43 | :ivar path: path to the library 44 | :vartype path: Optional[pathlib.Path] 45 | 46 | :ivar mem_address: hex memory location 47 | :vartype mem_address: Optional[str] 48 | 49 | :ivar unused: library not used 50 | :vartype unused: Optional[bool] 51 | """ 52 | 53 | @icontract.ensure( 54 | lambda self: all(hasattr(self, col) for col in DEPENDENCY_ATTRIBUTES), 55 | "All expected attributes are present so that " 56 | "lists of dependencies can be sorted dynamically") 57 | def __init__(self, 58 | found: bool, 59 | soname: Optional[str] = None, 60 | path: Optional[pathlib.Path] = None, 61 | mem_address: Optional[str] = None, 62 | unused: Optional[bool] = None) -> None: 63 | """Initialize the dependency with the given values.""" 64 | # pylint: disable= too-many-arguments 65 | self.soname = soname 66 | self.path = path 67 | self.found = found 68 | self.mem_address = mem_address 69 | self.unused = unused 70 | 71 | def __str__(self) -> str: 72 | """Transform the dependency to a human-readable format.""" 73 | return "soname: {}, path: {}, found: {}, mem_address: {}, unused: {}" \ 74 | "".format(self.soname, self.path, self.found, 75 | self.mem_address, self.unused) 76 | 77 | # pylint: disable=unsubscriptable-object 78 | def as_mapping(self) -> Mapping[str, Any]: 79 | """ 80 | Transform the dependency to a mapping. 81 | 82 | Can be converted to JSON and similar formats. 83 | """ 84 | return collections.OrderedDict([("soname", self.soname), 85 | ("path", str(self.path)), 86 | ("found", self.found), 87 | ("mem_address", self.mem_address), 88 | ("unused", self.unused)]) 89 | 90 | 91 | _LDD_ARROW_OUTPUT_RE = re.compile( 92 | r"(?P.+)\s=>\s(?P.*)\s\(?(?P\w*)\)?") 93 | _LDD_NON_ARROW_OUTPUT_RE = re.compile( 94 | r"(?P.+)\s\(?(?P\w*)\)?") 95 | 96 | 97 | # pylint: disable=too-many-branches 98 | def _parse_line(line: str) -> Optional[Dependency]: 99 | """ 100 | Parse single line of ldd output. 101 | 102 | :param line: to parse 103 | :return: dependency or None if line was empty 104 | 105 | """ 106 | # pylint: disable=line-too-long 107 | # There are two types of outputs for a dependency, with or without soname. 108 | # The VDSO is a special case (see https://man7.org/linux/man-pages/man7/vdso.7.html) 109 | # 110 | # For example: 111 | # VDSO (Ubuntu 16.04): linux-vdso.so.1 => (0x00007ffd7c7fd000) 112 | # VDSO (Ubuntu 18.04): linux-vdso.so.1 (0x00007ffe2f993000) 113 | # with soname: 'libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f9a19d8a000)' 114 | # without soname: '/lib64/ld-linux-x86-64.so.2 (0x00007f9a1a329000)' 115 | # with soname but not found: 'libboost_program_options.so.1.62.0 => not found' 116 | # with soname but without rpath: 'linux-vdso.so.1 => (0x00007ffd7c7fd000)' 117 | # pylint: enable=line-too-long 118 | found = not 'not found' in line 119 | soname = None 120 | dep_path = None 121 | mem_address = None 122 | if '=>' in line: 123 | mtch = _LDD_ARROW_OUTPUT_RE.match(line) 124 | if not mtch: 125 | raise RuntimeError(("Unexpected ldd output. Expected to match {}, " 126 | "but got: {!r}").format( 127 | _LDD_ARROW_OUTPUT_RE.pattern, line)) 128 | if found: 129 | soname = mtch["soname"] 130 | if mtch["dep_path"]: 131 | dep_path = pathlib.Path(mtch["dep_path"]) 132 | if mtch["mem_address"]: 133 | mem_address = mtch["mem_address"] 134 | else: 135 | if os.sep in mtch["soname"]: 136 | # This is a special case where the dep_path comes before the 137 | # arrow and we have no soname 138 | dep_path = pathlib.Path(mtch["soname"]) 139 | else: 140 | soname = mtch["soname"] 141 | else: 142 | # Please see https://github.com/Parquery/pylddwrap/pull/14 143 | if 'no version information available' in line: 144 | return None 145 | 146 | mtch = _LDD_NON_ARROW_OUTPUT_RE.match(line) 147 | if not mtch: 148 | raise RuntimeError(("Unexpected ldd output. Expected to match {}, " 149 | "but got: {!r}").format( 150 | _LDD_NON_ARROW_OUTPUT_RE.pattern, line)) 151 | # Special case for linux-vdso 152 | if mtch["dep_path"].startswith("linux-vdso"): 153 | soname = mtch["dep_path"] 154 | else: 155 | dep_path = pathlib.Path(mtch["dep_path"]) 156 | 157 | found = True 158 | mem_address = mtch["mem_address"] 159 | 160 | # Sanity check to see if it didn't parse garbage: 161 | # dep_path should have at least a `/` somewhere in the filepath 162 | if dep_path and os.sep not in str(dep_path): 163 | raise RuntimeError("Unexpected library path: {}".format(dep_path)) 164 | 165 | return Dependency( 166 | soname=soname, path=dep_path, found=found, mem_address=mem_address) 167 | 168 | 169 | @icontract.require(lambda path: path.is_file()) 170 | def list_dependencies(path: pathlib.Path, 171 | unused: bool = False, 172 | env: Optional[Dict[str, str]] = None) -> List[Dependency]: 173 | """ 174 | Retrieve a list of dependencies of the given binary. 175 | 176 | >>> import shutil 177 | >>> ls = shutil.which("ls") 178 | >>> path = pathlib.Path(ls) 179 | >>> deps = list_dependencies(path=path) 180 | >>> deps[0].soname 181 | 'linux-vdso.so.1' 182 | 183 | :param path: path to a file 184 | :param unused: 185 | if set, check if dependencies are actually used by the program 186 | :param env: 187 | the environment to use. 188 | 189 | If ``env`` is None, currently active env will be used. 190 | Otherwise specified env is used. 191 | :return: list of dependencies 192 | """ 193 | # We need to use /usr/bin/env since Popen ignores the PATH, 194 | # see https://stackoverflow.com/questions/5658622 195 | proc = subprocess.Popen( 196 | ["/usr/bin/env", "ldd", path.as_posix()], 197 | stdout=subprocess.PIPE, 198 | stderr=subprocess.PIPE, 199 | universal_newlines=True, 200 | env=env) 201 | 202 | out, err = proc.communicate() 203 | if proc.returncode != 0: 204 | raise RuntimeError( 205 | "Failed to ldd external libraries of {} with code {}:\nout:\n{}\n\n" 206 | "err:\n{}".format(path, proc.returncode, out, err)) 207 | 208 | dependencies = _cmd_output_parser(cmd_out=out) # type: List[Dependency] 209 | 210 | if unused: 211 | proc_unused = subprocess.Popen( 212 | ["/usr/bin/env", "ldd", "--unused", 213 | path.as_posix()], 214 | stdout=subprocess.PIPE, 215 | stderr=subprocess.PIPE, 216 | universal_newlines=True, 217 | env=env) 218 | 219 | out_unused, err_unused = proc_unused.communicate() 220 | # return code = 0 -> no unused dependencies, 221 | # return code = 1 -> some unused dependencies 222 | if proc_unused.returncode not in [0, 1]: 223 | raise RuntimeError( 224 | "Failed to ldd external libraries of {} with code {}:\nout:\n" 225 | "{}\n\nerr:\n{}".format(path, proc.returncode, out_unused, 226 | err_unused)) 227 | 228 | dependencies = _update_unused( 229 | dependencies=dependencies, out_unused=out_unused) 230 | 231 | return dependencies 232 | 233 | 234 | def _cmd_output_parser(cmd_out: str) -> List[Dependency]: 235 | """ 236 | Parse the command line output. 237 | 238 | :param cmd_out: command line output 239 | :return: List of dependencies 240 | """ 241 | dependencies = [] # type: List[Dependency] 242 | 243 | lines = [line.strip() for line in cmd_out.split('\n') if line.strip() != ''] 244 | 245 | if len(lines) == 0: 246 | return [] 247 | 248 | # Static libraries can appear in multiple forms: 249 | # - a first line refering to the library and a second line indicating 250 | # that the library was statically linked, 251 | # - only a single line indicating statically linked. 252 | # See: https://github.com/Parquery/pylddwrap/issues/12 253 | if 1 <= len(lines) <= 2 and lines[-1] == 'statically linked': 254 | return [] 255 | 256 | for line in lines: 257 | dep = _parse_line(line=line) 258 | if dep is not None: 259 | dependencies.append(dep) 260 | 261 | return dependencies 262 | 263 | 264 | def _update_unused(dependencies: List[Dependency], 265 | out_unused: str) -> List[Dependency]: 266 | """ 267 | Set "unused" property of the dependencies. 268 | 269 | Updates the "unused" property of the dependencies using the output string 270 | from ldd command. 271 | 272 | :param dependencies: List of dependencies 273 | :param out_unused: output from command ldd --unused 274 | :return: updated list of dependencies 275 | """ 276 | unused_dependencies = [] # type: List[pathlib.Path] 277 | 278 | for line in [ 279 | line.strip() for line in out_unused.split('\n') 280 | if line.strip() != '' 281 | ]: 282 | # skip first line because it's no dependency 283 | if line != "Unused direct dependencies:": 284 | unused_dependencies.append(pathlib.Path(line.strip())) 285 | 286 | for dep in dependencies: 287 | dep.unused = dep.path in unused_dependencies 288 | 289 | return dependencies 290 | 291 | 292 | # pylint: disable=unnecessary-lambda 293 | @icontract.require(lambda sort_by: sort_by in DEPENDENCY_ATTRIBUTES) 294 | @icontract.snapshot( 295 | lambda deps: copy.copy(deps), name='deps', enabled=icontract.SLOW) 296 | @icontract.ensure( 297 | lambda deps, OLD: set(deps) == set(OLD.deps), enabled=icontract.SLOW) 298 | # pylint: enable=line-too-long 299 | def _sort_dependencies_in_place(deps: List[Dependency], sort_by: str) -> None: 300 | """Order the dependencies by the given ``sort_by`` attribute.""" 301 | if len(deps) < 2: 302 | return 303 | 304 | def compute_key(dep: Dependency) -> str: 305 | """Produce key for the sort.""" 306 | assert hasattr(dep, sort_by) 307 | key = getattr(dep, sort_by) 308 | 309 | assert key is None or isinstance(key, (bool, str, pathlib.Path)) 310 | 311 | if key is None: 312 | return '' 313 | 314 | return str(key) 315 | 316 | deps.sort(key=compute_key) 317 | 318 | 319 | def _output_verbose(deps: List[Dependency], stream: TextIO) -> None: 320 | """ 321 | Output dependencies in verbose, human-readable format to the ``stream``. 322 | 323 | :param deps: list of dependencies 324 | :param stream: output stream 325 | :return: 326 | """ 327 | table = [["soname", "path", "found", "mem_address", "unused"]] 328 | 329 | for dep in deps: 330 | table.append([ 331 | str(dep.soname), 332 | str(dep.path), 333 | str(dep.found), 334 | str(dep.mem_address), 335 | str(dep.unused) 336 | ]) 337 | 338 | stream.write(_format_table(table)) 339 | 340 | 341 | def _output_json(deps: List[Dependency], stream: TextIO) -> None: 342 | """ 343 | Output dependencies in a JSON format to the ``stream``. 344 | 345 | :param deps: list of dependencies 346 | :param stream: output stream 347 | :return: 348 | """ 349 | json.dump(obj=[dep.as_mapping() for dep in deps], fp=stream, indent=2) 350 | 351 | 352 | @icontract.require( 353 | lambda table: not table or all(len(row) == len(table[0]) for row in table)) 354 | @icontract.ensure(lambda table, result: result == "" if not table else True) 355 | @icontract.ensure(lambda result: not result.endswith("\n")) 356 | def _format_table(table: List[List[str]]) -> str: 357 | """ 358 | Format the table as equal-spaced columns. 359 | 360 | :param table: rows of cells 361 | :return: table as string 362 | """ 363 | cols = len(table[0]) 364 | 365 | col_widths = [max(len(row[i]) for row in table) for i in range(cols)] 366 | 367 | lines = [] # type: List[str] 368 | for i, row in enumerate(table): 369 | parts = [] # type: List[str] 370 | 371 | for cell, width in zip(row, col_widths): 372 | parts.append(cell.ljust(width)) 373 | 374 | line = " | ".join(parts) 375 | lines.append(line) 376 | 377 | if i == 0: 378 | border = [] # type: List[str] 379 | 380 | for width in col_widths: 381 | border.append("-" * width) 382 | 383 | lines.append("-+-".join(border)) 384 | 385 | result = "\n".join(lines) 386 | 387 | return result 388 | -------------------------------------------------------------------------------- /tests/test_ldd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Test lddwrap.""" 3 | # pylint: disable=missing-docstring,too-many-public-methods 4 | import pathlib 5 | import shutil 6 | import tempfile 7 | import unittest 8 | from typing import Any, List, Optional 9 | 10 | import lddwrap 11 | 12 | import tests 13 | 14 | # Some distros like NixOS does not have binaries on /bin. 15 | # Instead of hardcoding them, try to get them from PATH 16 | # using shutil.which function. 17 | DIR = shutil.which("dir") or "/bin/dir" 18 | PWD = shutil.which("pwd") or "/bin/pwd" 19 | 20 | 21 | class DependencyDiff: 22 | """Represent a different between two dependencies.""" 23 | 24 | def __init__(self, attribute: str, ours: Any, theirs: Any) -> None: 25 | self.attribute = attribute 26 | self.ours = ours 27 | self.theirs = theirs 28 | 29 | def __repr__(self) -> str: 30 | return "DependencyDiff(attribute={!r}, ours={!r}, theirs={!r})".format( 31 | self.attribute, self.ours, self.theirs) 32 | 33 | 34 | def diff_dependencies(ours: lddwrap.Dependency, 35 | theirs: lddwrap.Dependency) -> List[DependencyDiff]: 36 | """ 37 | Compare two dependencies and give their differences in a list. 38 | 39 | An empty list means no difference. 40 | """ 41 | keys = sorted(ours.__dict__.keys()) 42 | assert keys == sorted(theirs.__dict__.keys()) 43 | 44 | result = [] # type: List[DependencyDiff] 45 | for key in keys: 46 | if ours.__dict__[key] != theirs.__dict__[key]: 47 | result.append( 48 | DependencyDiff( 49 | attribute=key, 50 | ours=ours.__dict__[key], 51 | theirs=theirs.__dict__[key])) 52 | 53 | return result 54 | 55 | 56 | class TestParseOutputWithoutUnused(unittest.TestCase): 57 | def test_parse_line(self): 58 | # yapf: disable 59 | lines = [ 60 | "libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 " 61 | "(0x00007f9a19d8a000)", 62 | "/lib64/ld-linux-x86-64.so.2 (0x00007f9a1a329000)", 63 | "libboost_program_options.so.1.62.0 => not found", 64 | "linux-vdso.so.1 => (0x00007ffd7c7fd000)", 65 | "libopencv_stitching.so.3.3 => not found", 66 | "libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 " 67 | "(0x00007f4b78462000)", 68 | "libz.so.1 => not found", 69 | "../build/debug/libextstr.so => not found", 70 | "/home/user/lib/liblmdb.so => not found", 71 | "/home/u s e r/lib/liblmdb.so => not found", 72 | "UnityPlayer.so => /home/user/games/q u d/./UnityPlayer.so " 73 | "(0x00007f90f290f000)" 74 | ] 75 | # yapf: enable 76 | 77 | expected_deps = [ 78 | lddwrap.Dependency( 79 | soname="libstdc++.so.6", 80 | path=pathlib.Path("/usr/lib/x86_64-linux-gnu/libstdc++.so.6"), 81 | found=True, 82 | mem_address="0x00007f9a19d8a000", 83 | unused=None), 84 | lddwrap.Dependency( 85 | soname=None, 86 | path=pathlib.Path("/lib64/ld-linux-x86-64.so.2"), 87 | found=True, 88 | mem_address="0x00007f9a1a329000", 89 | unused=None), 90 | lddwrap.Dependency( 91 | soname="libboost_program_options.so.1.62.0", 92 | path=None, 93 | found=False, 94 | mem_address=None, 95 | unused=None), 96 | lddwrap.Dependency( 97 | soname="linux-vdso.so.1", 98 | path=None, 99 | found=True, 100 | mem_address="0x00007ffd7c7fd000", 101 | unused=None), 102 | lddwrap.Dependency( 103 | soname="libopencv_stitching.so.3.3", 104 | path=None, 105 | found=False, 106 | mem_address=None, 107 | unused=None), 108 | lddwrap.Dependency( 109 | soname="libstdc++.so.6", 110 | path=pathlib.Path("/usr/lib/x86_64-linux-gnu/libstdc++.so.6"), 111 | found=True, 112 | mem_address="0x00007f4b78462000", 113 | unused=None), 114 | lddwrap.Dependency( 115 | soname="libz.so.1", path=None, found=False, mem_address=None), 116 | lddwrap.Dependency( 117 | soname=None, 118 | path=pathlib.Path("../build/debug/libextstr.so"), 119 | found=False, 120 | mem_address=None, 121 | unused=None), 122 | lddwrap.Dependency( 123 | soname=None, 124 | path=pathlib.Path("/home/user/lib/liblmdb.so"), 125 | found=False, 126 | mem_address=None, 127 | unused=None), 128 | lddwrap.Dependency( 129 | soname=None, 130 | path=pathlib.Path("/home/u s e r/lib/liblmdb.so"), 131 | found=False, 132 | mem_address=None, 133 | unused=None), 134 | lddwrap.Dependency( 135 | soname="UnityPlayer.so", 136 | path=pathlib.Path("/home/user/games/q u d/./UnityPlayer.so"), 137 | found=True, 138 | mem_address="0x00007f90f290f000", 139 | unused=None), 140 | ] 141 | 142 | for i, line in enumerate(lines): 143 | # pylint: disable=protected-access 144 | dep = lddwrap._parse_line(line=line) 145 | 146 | self.assertListEqual( 147 | [], diff_dependencies(ours=dep, theirs=expected_deps[i]), 148 | "Incorrect dependency read from the line {} {!r}".format( 149 | i + 1, line)) 150 | 151 | def test_parse_wrong_line(self): 152 | # ``parse_line`` raises a RuntimeError when it receives an unexpected 153 | # structured line 154 | run_err = None # type: Optional[RuntimeError] 155 | line = "\tsome wrong data which does not make sense" 156 | try: 157 | lddwrap._parse_line(line=line) # pylint: disable=protected-access 158 | except RuntimeError as err: 159 | run_err = err 160 | 161 | self.assertIsNotNone(run_err) 162 | self.assertTrue(str(run_err).startswith("Unexpected library path:")) 163 | 164 | def test_parse_non_indented_line(self): 165 | """Lines without leading indentation, at this point in processing, are 166 | informational. 167 | """ 168 | # https://github.com/Parquery/pylddwrap/pull/14 169 | line = ( 170 | "qt/6.0.2/gcc_64/plugins/sqldrivers/libqsqlpsql.so:" + 171 | " /lib/x86_64-linux-gnu/libpq.so.5:" + 172 | " no version information available" + 173 | " (required by qt/6.0.2/gcc_64/plugins/sqldrivers/libqsqlpsql.so)") 174 | result = lddwrap._parse_line(line=line) # pylint: disable=protected-access 175 | 176 | self.assertIsNone(result) 177 | 178 | def test_parse_static_with_two_line_output(self) -> None: 179 | """Test parsing of the output when we ldd a static library.""" 180 | # pylint: disable=protected-access 181 | deps = lddwrap._cmd_output_parser("\n".join([ 182 | "my_static_lib.so:", 183 | " statically linked", 184 | "", 185 | ])) 186 | 187 | self.assertListEqual([], deps) 188 | 189 | def test_parse_static_with_single_line_output(self) -> None: 190 | """Test parsing of the output when we ldd a static library.""" 191 | # pylint: disable=protected-access 192 | deps = lddwrap._cmd_output_parser("\tstatically linked\n") 193 | 194 | self.assertListEqual([], deps) 195 | 196 | 197 | class TestAgainstMockLdd(unittest.TestCase): 198 | def test_pwd(self): 199 | """Test parsing the captured output of ``ldd`` on ``/bin/pwd``.""" 200 | 201 | with tests.MockLdd( 202 | out="\n".join([ 203 | "\tlinux-vdso.so.1 (0x00007ffe0953f000)", 204 | "\tlibc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd548353000)", # pylint: disable=C0301 # pylint: disable=C0301 205 | "\t/lib64/ld-linux-x86-64.so.2 (0x00007fd54894d000)", 206 | "", 207 | ]), 208 | out_unused=''): 209 | deps = lddwrap.list_dependencies( 210 | path=pathlib.Path(PWD), unused=False) 211 | 212 | expected_deps = [ 213 | lddwrap.Dependency( 214 | soname="linux-vdso.so.1", 215 | path=None, 216 | found=True, 217 | mem_address="0x00007ffe0953f000", 218 | unused=None), 219 | lddwrap.Dependency( 220 | soname='libc.so.6', 221 | path=pathlib.Path("/lib/x86_64-linux-gnu/libc.so.6"), 222 | found=True, 223 | mem_address="0x00007fd548353000", 224 | unused=None), 225 | lddwrap.Dependency( 226 | soname=None, 227 | path=pathlib.Path("/lib64/ld-linux-x86-64.so.2"), 228 | found=True, 229 | mem_address="0x00007fd54894d000", 230 | unused=None) 231 | ] 232 | 233 | self.assertEqual(len(expected_deps), len(deps)) 234 | 235 | for i, (dep, expected_dep) in enumerate(zip(deps, expected_deps)): 236 | self.assertListEqual([], 237 | diff_dependencies( 238 | ours=dep, theirs=expected_dep), 239 | "Mismatch at the dependency {}".format(i)) 240 | 241 | def test_bin_dir(self): 242 | """Test parsing the captured output of ``ldd`` on ``/bin/dir``.""" 243 | 244 | # pylint: disable=line-too-long 245 | with tests.MockLdd( 246 | out="\n".join([ 247 | "\tlinux-vdso.so.1 (0x00007ffd66ce2000)", 248 | "\tlibselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f72b88fc000)", 249 | "\tlibc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f72b850b000)", 250 | "\tlibpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007f72b8299000)", 251 | "\tlibdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f72b8095000)", 252 | "\t/lib64/ld-linux-x86-64.so.2 (0x00007f72b8d46000)", 253 | "\tlibpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f72b7e76000)", 254 | "", 255 | ]), 256 | out_unused=''): 257 | # pylint: enable=line-too-long 258 | deps = lddwrap.list_dependencies( 259 | path=pathlib.Path(DIR), unused=False) 260 | 261 | expected_deps = [ 262 | lddwrap.Dependency( 263 | soname="linux-vdso.so.1", 264 | path=None, 265 | found=True, 266 | mem_address="0x00007ffd66ce2000", 267 | unused=None), 268 | lddwrap.Dependency( 269 | soname="libselinux.so.1", 270 | path=pathlib.Path("/lib/x86_64-linux-gnu/libselinux.so.1"), 271 | found=True, 272 | mem_address="0x00007f72b88fc000", 273 | unused=None), 274 | lddwrap.Dependency( 275 | soname="libc.so.6", 276 | path=pathlib.Path("/lib/x86_64-linux-gnu/libc.so.6"), 277 | found=True, 278 | mem_address="0x00007f72b850b000", 279 | unused=None), 280 | lddwrap.Dependency( 281 | soname="libpcre.so.3", 282 | path=pathlib.Path("/lib/x86_64-linux-gnu/libpcre.so.3"), 283 | found=True, 284 | mem_address="0x00007f72b8299000", 285 | unused=None), 286 | lddwrap.Dependency( 287 | soname="libdl.so.2", 288 | path=pathlib.Path("/lib/x86_64-linux-gnu/libdl.so.2"), 289 | found=True, 290 | mem_address="0x00007f72b8095000", 291 | unused=None), 292 | lddwrap.Dependency( 293 | soname=None, 294 | path=pathlib.Path("/lib64/ld-linux-x86-64.so.2"), 295 | found=True, 296 | mem_address="0x00007f72b8d46000", 297 | unused=None), 298 | lddwrap.Dependency( 299 | soname="libpthread.so.0", 300 | path=pathlib.Path("/lib/x86_64-linux-gnu/libpthread.so.0"), 301 | found=True, 302 | mem_address="0x00007f72b7e76000", 303 | unused=None), 304 | ] 305 | 306 | self.assertEqual(len(expected_deps), len(deps)) 307 | 308 | for i, (dep, expected_dep) in enumerate(zip(deps, expected_deps)): 309 | self.assertListEqual([], 310 | diff_dependencies( 311 | ours=dep, theirs=expected_dep), 312 | "Mismatch at the dependency {}".format(i)) 313 | 314 | def test_bin_dir_with_empty_unused(self): 315 | # pylint: disable=line-too-long 316 | with tests.MockLdd( 317 | out="\n".join([ 318 | "\tlinux-vdso.so.1 (0x00007ffd66ce2000)", 319 | "\tlibselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f72b88fc000)", 320 | "\tlibc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f72b850b000)", 321 | "\tlibpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007f72b8299000)", 322 | "\tlibdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f72b8095000)", 323 | "\t/lib64/ld-linux-x86-64.so.2 (0x00007f72b8d46000)", 324 | "\tlibpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f72b7e76000)", 325 | "", 326 | ]), 327 | out_unused=''): 328 | # pylint: enable=line-too-long 329 | deps = lddwrap.list_dependencies( 330 | path=pathlib.Path(DIR), unused=True) 331 | 332 | unused = [dep for dep in deps if dep.unused] 333 | self.assertListEqual([], unused) 334 | 335 | def test_with_fantasy_unused(self): 336 | """Test against a fantasy executable with fantasy unused.""" 337 | # pylint: disable=line-too-long 338 | with tests.MockLdd( 339 | out="\n".join([ 340 | "\tlinux-vdso.so.1 (0x00007ffd66ce2000)", 341 | "\tlibm.so.6 => /lib64/libm.so.6 (0x00007f72b7e76000)", 342 | "", 343 | ]), 344 | out_unused="\n".join([ 345 | "Unused direct dependencies:", 346 | "\t/lib64/libm.so.6", 347 | "", 348 | ]), 349 | ): 350 | # pylint: enable=line-too-long 351 | deps = lddwrap.list_dependencies( 352 | path=pathlib.Path(DIR), unused=True) 353 | 354 | unused = [dep for dep in deps if dep.unused] 355 | 356 | expected_unused = [ 357 | lddwrap.Dependency( 358 | soname="libm.so.6", 359 | path=pathlib.Path("/lib64/libm.so.6"), 360 | found=True, 361 | mem_address="0x00007f72b7e76000", 362 | unused=True) 363 | ] 364 | 365 | self.assertEqual(len(expected_unused), len(unused)) 366 | 367 | for i, (dep, exp_dep) in enumerate(zip(unused, expected_unused)): 368 | self.assertListEqual( 369 | [], diff_dependencies(ours=dep, theirs=exp_dep), 370 | "Mismatch at the unused dependency {}".format(i)) 371 | 372 | def test_with_static_library(self) -> None: 373 | """Test against a fantasy static library.""" 374 | with tempfile.TemporaryDirectory() as tmp_dir: 375 | lib_pth = pathlib.Path(tmp_dir) / "my_static_lib.so" 376 | lib_pth.write_text("totally static!") 377 | 378 | with tests.MockLdd( 379 | out="\n".join([ 380 | "my_static_lib.so:", 381 | "\tstatically linked", 382 | "", 383 | ]), 384 | out_unused=''): 385 | # pylint: enable=line-too-long 386 | deps = lddwrap.list_dependencies(path=lib_pth, unused=True) 387 | 388 | # The dependencies are empty since the library is 389 | # statically linked. 390 | self.assertListEqual([], deps) 391 | 392 | 393 | class TestSorting(unittest.TestCase): 394 | def test_sorting_by_all_attributes(self) -> None: 395 | # pylint: disable=line-too-long 396 | with tests.MockLdd( 397 | out="\n".join([ 398 | "\tlinux-vdso.so.1 (0x00007ffd66ce2000)", 399 | "\tlibselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f72b88fc000)", 400 | "\tlibc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f72b850b000)", 401 | "\tlibpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007f72b8299000)", 402 | "\tlibdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f72b8095000)", 403 | "\t/lib64/ld-linux-x86-64.so.2 (0x00007f72b8d46000)", 404 | "\tlibpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f72b7e76000)", 405 | "", 406 | ]), 407 | out_unused=''): 408 | # pylint: enable=line-too-long 409 | 410 | for attr in lddwrap.DEPENDENCY_ATTRIBUTES: 411 | deps = lddwrap.list_dependencies( 412 | path=pathlib.Path(DIR), unused=True) 413 | 414 | # pylint: disable=protected-access 415 | lddwrap._sort_dependencies_in_place(deps=deps, sort_by=attr) 416 | 417 | previous = getattr(deps[0], attr) 418 | previous = '' if previous is None else str(previous) 419 | 420 | for i in range(1, len(deps)): 421 | current = getattr(deps[i], attr) 422 | current = '' if current is None else str(current) 423 | 424 | self.assertLessEqual( 425 | previous, current, 426 | ("The dependencies must be sorted according to " 427 | "attribute {!r}: {!r}").format(attr, deps)) 428 | 429 | 430 | if __name__ == '__main__': 431 | unittest.main() 432 | --------------------------------------------------------------------------------