diff --git a/Namcap/rules/__init__.py b/Namcap/rules/__init__.py index 525dbc6..01d1b96 100644 --- a/Namcap/rules/__init__.py +++ b/Namcap/rules/__init__.py @@ -43,6 +43,7 @@ from . import ( perllocal, permissions, py_mtime, + pydepends, rpath, scrollkeeper, shebangdepends, diff --git a/Namcap/rules/pydepends.py b/Namcap/rules/pydepends.py new file mode 100644 index 0000000..efc6735 --- /dev/null +++ b/Namcap/rules/pydepends.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +# +# namcap rules - pydepends +# Copyright (C) 2020 Felix Yan +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +from collections import defaultdict +import ast +import sys +import sysconfig +import Namcap.package +from Namcap.ruleclass import * + + +def finddepends(liblist): + """ + Find packages owning a list of libraries + + Returns: + dependlist -- a dictionary { package => set(libraries) } + orphans -- the list of libraries without owners + """ + dependlist = defaultdict(set) + + pymatches = {} + + knownlibs = set(liblist) + foundlibs = set() + + workarounds = { + "python": sys.builtin_module_names + } + + for pkg in Namcap.package.get_installed_packages(): + for j, fsize, fmode in pkg.files: + if not j.startswith("usr/lib/python3"): + continue + + for k in knownlibs: + if j.endswith("site-packages/" + k + "/") or j.endswith("site-packages/" + k + ".py") or \ + j.endswith("site-packages/" + k + ".so") or \ + j.endswith("site-packages/" + k + sysconfig.get_config_var('EXT_SUFFIX')) or \ + j.endswith("lib-dynload/" + k + sysconfig.get_config_var('EXT_SUFFIX')) or \ + j.count("/") == 3 and j.endswith("/" + k + ".py") or \ + j.count("/") == 4 and j.endswith("/" + k + "/") or \ + pkg.name in workarounds and k in workarounds[pkg.name]: + dependlist[pkg.name].add(k) + foundlibs.add(k) + + orphans = list(knownlibs - foundlibs) + return dependlist, orphans + + +def get_imports(file): + root = ast.parse(file.read()) + + for node in ast.walk(root): + if isinstance(node, ast.Import): + for module in node.names: + yield module.name.split('.')[0] + elif isinstance(node, ast.ImportFrom): + if node.module and node.level == 0: + yield node.module.split('.')[0] + + +class PythonDependencyRule(TarballRule): + name = "pydepends" + description = "Checks python dependencies" + def analyze(self, pkginfo, tar): + liblist = defaultdict(set) + own_liblist = set() + + for entry in tar: + if not entry.isfile() or not entry.name.endswith('.py'): + continue + own_liblist.add(entry.name[:-3]) + f = tar.extractfile(entry) + for module in get_imports(f): + liblist[module].add(entry.name) + f.close() + + for lib in own_liblist: + liblist.pop(lib, None) + + dependlist, orphans = finddepends(liblist) + + # Handle "no package associated" errors + self.warnings.extend([("library-no-package-associated %s", i) + for i in orphans]) + + # Print link-level deps + for pkg, libraries in dependlist.items(): + if isinstance(libraries, set): + files = list(libraries) + needing = set().union(*[liblist[lib] for lib in libraries]) + reasons = pkginfo.detected_deps.setdefault(pkg, []) + reasons.append(( + "libraries-needed %s %s", + (str(files), str(list(needing))) + )) + self.infos.append(("link-level-dependence %s in %s", (pkg, str(files)))) + + # Check for packages in testing + for i in dependlist.keys(): + p = Namcap.package.load_testing_package(i) + q = Namcap.package.load_from_db(i) + if p is not None and q is not None and p["version"] == q["version"] : + self.warnings.append(("dependency-is-testing-release %s", i)) diff --git a/Namcap/tests/package/test_pydepends.py b/Namcap/tests/package/test_pydepends.py new file mode 100644 index 0000000..bb0dfbb --- /dev/null +++ b/Namcap/tests/package/test_pydepends.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# +# namcap tests - pydepends +# Copyright (C) 2020 Felix Yan +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +import os +from Namcap.tests.makepkg import MakepkgTest +import Namcap.rules.pydepends + + +class PyDependsTest(MakepkgTest): + pkgbuild = """ +pkgname=__namcap_test_pydepends +pkgver=1.0 +pkgrel=1 +pkgdesc="A package" +arch=('any') +url="http://www.example.com/" +license=('GPL') +depends=('python-six') +source=() +build() { + cd "${srcdir}" + echo "import six, pyalpm" > main.py +} +package() { + install -D -m 755 "$srcdir/main.py" "$pkgdir/usr/bin/main.py" +} +""" + def test_pydepends(self): + "Package with missing pacman dependency" + pkgfile = "__namcap_test_pydepends-1.0-1-any.pkg.tar" + with open(os.path.join(self.tmpdir, "PKGBUILD"), "w") as f: + f.write(self.pkgbuild) + self.run_makepkg() + pkg, r = self.run_rule_on_tarball( + os.path.join(self.tmpdir, pkgfile), + Namcap.rules.pydepends.PythonDependencyRule + ) + self.assertEqual(pkg.detected_deps['pyalpm'], [ + ('libraries-needed %s %s', + (str(["pyalpm"]), str(["usr/bin/main.py"]))) + ] + ) + e, w, i = Namcap.depends.analyze_depends(pkg) + self.assertEqual(e, [ + ('dependency-detected-not-included %s (%s)', + ('pyalpm', "libraries ['pyalpm'] needed in files ['usr/bin/main.py']")) + ]) + self.assertEqual(w, []) + +# vim: set ts=4 sw=4 noet: