@@ -43,6 +43,7 @@ from . import (
perllocal,
permissions,
py_mtime,
+ pydepends,
rpath,
scrollkeeper,
shebangdepends,
new file mode 100644
@@ -0,0 +1,122 @@
+# -*- coding: utf-8 -*-
+#
+# namcap rules - pydepends
+# Copyright (C) 2020 Felix Yan <felixonmars@archlinux.org>
+#
+# 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))
new file mode 100644
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+#
+# namcap tests - pydepends
+# Copyright (C) 2020 Felix Yan <felixonmars@archlinux.org>
+#
+# 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: