diff options
Diffstat (limited to 'exts/pip_find_deps.py')
-rwxr-xr-x | exts/pip_find_deps.py | 245 |
1 files changed, 245 insertions, 0 deletions
diff --git a/exts/pip_find_deps.py b/exts/pip_find_deps.py new file mode 100755 index 0000000..3666afe --- /dev/null +++ b/exts/pip_find_deps.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python +# +# Find the build and runtime dependencies for a given Python package +# +# Copyright (C) 2014 Codethink Limited +# +# 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; version 2 of the License. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from __future__ import print_function + +import sys +import subprocess +import os + +import pkg_resources +import xmlrpclib + +DEBUG = False + +# TODO: I'm guessing these things are probably standard somewhere +def warn(*args, **kwargs): + print('%s:' % sys.argv[0], *args, file=sys.stderr, **kwargs) + +def error(*args, **kwargs): + warn(*args, **kwargs) + sys.exit(1) + +def debug(s): + if DEBUG: + print(s) + +# TODO: this is copied from pip_lorry, it should be shared. +def specs_satisfied(version, specs): + def mapping_error(op): + # We parse ops with requirements-parser, so any invalid user input + # should be detected there. This really guards against + # the pip developers adding some new operation to a requirement. + error("Invalid op in spec: %s" % op) + + opmap = {'==' : lambda x, y: x == y, '!=' : lambda x, y: x != y, + '<=' : lambda x, y: x <= y, '>=' : lambda x, y: x >= y, + '<': lambda x, y: x < y, '>' : lambda x, y: x > y} + + def get_op_func(op): + return opmap[op] if op in opmap else lambda x, y: mapping_error(op) + + return all([get_op_func(op)(version, sv) for (op, sv) in specs]) + +def conflict(specs, spec): + # Return whether spec_x conflicts with spec_y + # examples: ('==', '0.1') conflicts with ('==', '0.2') + + # ('==', '0.1') conflicts with ('<', 0.1) + # ('==', '0.1') conflicts with ('>', 0.1) + # ('==', '0.1') conflicts with ('!=', 0.1) + + # ('<', '0.1') conflicts with ('>', '0.1') + # ('<=', 0.1) conflicts with ('>=', 0.2) + # ('<', '0.1') conflicts with ('>', '0.2') + pass + +class Dependency(object): + def __init__(self): + # non-inclusive boundaries + self.less_than = None + self.greater_than = None + + self.absolute = None + self.excludes = [] + + # A list of conclicting specs for this dependency + self.conflicts = [] + + def is_unbounded(self): + return self.less_than == None and self.greater_than == None + + def _in_less_than(self, version): + return True if self.less_than == None else version < self.less_than + + def _in_greater_than(self, version): + return True if self.greater_than == None else version > self.greater_than + + def in_bounds(self, version): + return self.is_unbounded() or (self._in_less_than(version) + and self._in_greater_than(version)) + + def get_bounds_conflict(self, version): + if not self._in_less_than(version): + return ('<', str(self.less_than)) + elif not self._in_greater_than(version): + return ('>', str(self.greater_than)) + else: + return None + + def is_unconstrained(self): + return (self.is_unbounded() and self.excludes == [] + and self.absolute == None) + + def set_absolute_version(self, version): + self.absolute = version + + +def check_eqs(dep, version): + if dep.is_unconstrained(): + dep.set_absolute_version(version) + elif version not in dep.excludes: + if dep.absolute in [version, None]: + if dep.in_bounds(version): + dep.set_absolute_version(version) + else: + warn('conflict! == %s conflicts with %s' + % (version, + dep.get_bounds_conflict(version))) + dep.conflicts.append((('==', version), + dep.get_bounds_conflict(version))) + + else: + warn('conflict! == %s conflicts with == %s' + % (version, dep.absolute)) + dep.conflicts.append(('==', version), ('!=', dep.absolute)) + elif version in dep.excludes: + warn('conflict! == %s conflicts with != %s' + % (version, version)) + dep.conflicts.append((('==', version), ('!=', version))) + elif version > dep.less_than: + # conflict + warn('conflict! == %s conflicts with < %s' + % (version, dep.less_than)) + dep.conflicts.append((('==', version), ('<', dep.less_than))) + elif version < dep.greater_than: + # conflict + warn('conflict! == %s conflicts with > %s' + % (version, dep.greater_than)) + dep.conflicts.append(('==', version), ('>', dep.greater_than)) + +def check_lt(dep, version): + if dep.is_unconstrained(): + dep.less_than = version + elif dep.greater_than >= version: + # conflict #(our minimum version is greater + # than this greater_than version) + warn('conflict! > %s conflicts with < %s' + % (dep.greater_than, version)) + dep.conflicts.append((('>', dep.greater_than), ('<', version))) + else: + dep.less_than = version + +def check_gt(dep, version): + if dep.is_unconstrained(): + dep.greater_than = version + elif dep.less_than <= version: + # conflict (our maximum version is less than this + # less_than version) + warn('conflict! < %s conflicts with > %s' + % (dep.less_than, version)) + dep.conflicts.append((('<', dep.less_than), ('>', version))) + +def resolve_version_constraints(requirements): + build_deps = {} + + for r in requirements: + print('%s %s' % (r.project_name, r.specs)) + + if r.project_name not in build_deps: + build_deps[r.project_name] = Dependency() + + for (op, version) in r.specs: + version = pkg_resources.parse_version(version) + + #if no_conflict(requirements_versions_map[r.project_name], s): + # requirements_versions_map[r.project_name] += s + dep = build_deps[r.project_name] + + print('dep.excludes: %s' % str(dep.excludes)) + + # TODO: replace with function table + if op == '==': + check_eqs(dep, version) + elif op == '!=': + dep.excludes.append(version) + # TODO: chk_eqs needed here + elif op == '<': + check_lt(dep, version) + elif op == '>': + check_gt(dep, version) + + # Resolve versions + #client = xmlrpclib.ServerProxy(PYPI_URL) + #releases = client.package_releases(requirement.name) + return build_deps + +def find_build_deps(source, name, version=None): + debug('source: %s' % source) + debug('name: %s' % name) + debug('version: %s' % version) + + # This amounts to running python setup.py egg_info and checking + # the resulting egg_info dir for a file called setup_requires.txt + + # So it's $name.egg_info + p = subprocess.Popen(['python', 'setup.py', 'egg_info'], cwd=source, + stdout=subprocess.PIPE) + + if p.wait() != 0: + error('egg_info command failed') + + egg_dir = '%s.egg-info' % name + build_deps_file = os.path.join(source, egg_dir, 'setup_requires.txt') + + build_deps = {} + + # Check whether there's a setup_requires.txt + if not os.path.isfile(build_deps_file): + print('%s has no build dependencies' % name) + else: + with open(build_deps_file) as f: + build_deps = resolve_version_constraints( + pkg_resources.parse_requirements(f)) + + return build_deps + +if len(sys.argv) not in [3, 4]: + print('usage: %s PACKAGE_SOURCE_DIR NAME [VERSION]' % sys.argv[0]) + sys.exit(1) + +# Ignore the issue of dependency conflicts for now + +# First, given a source return build dependencies in json from + +for name, dep in find_build_deps(*sys.argv[1:]).iteritems(): + print('%s less_than: %s greater_than: %s absolute: %s excludes: %s' + % (name, str(dep.less_than), str(dep.greater_than), + str(dep.absolute), dep.excludes)) + print('conflicts: %s' % str(dep.conflicts))
\ No newline at end of file |