diff options
Diffstat (limited to 'dhpython/depends.py')
-rw-r--r-- | dhpython/depends.py | 281 |
1 files changed, 281 insertions, 0 deletions
diff --git a/dhpython/depends.py b/dhpython/depends.py new file mode 100644 index 0000000..e17951c --- /dev/null +++ b/dhpython/depends.py @@ -0,0 +1,281 @@ +# Copyright © 2010-2013 Piotr Ożarowski <piotr@debian.org> +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import logging +from functools import partial +from os.path import exists, join +from dhpython import PKG_PREFIX_MAP, MINPYCDEP +from dhpython.pydist import parse_pydep, parse_requires_dist, guess_dependency +from dhpython.version import default, supported, VersionRange + +log = logging.getLogger('dhpython') + + +class Dependencies: + """Store relations (dependencies, etc.) between packages.""" + + def __init__(self, package, impl='cpython3', bdep=None): + self.impl = impl + self.package = package + bdep = self.bdep = bdep or {} + self.is_debug_package = dbgpkg = package.endswith('-dbg') + + # TODO: move it to PyPy and CPython{2,3} classes + self.ipkg_vtpl = 'python%s-dbg' if dbgpkg else 'python%s' + if impl == 'cpython3': + self.ipkg_tpl = 'python3-dbg' if dbgpkg else 'python3' + elif impl == 'cpython2': + self.ipkg_tpl = 'python2-dbg' if dbgpkg else 'python2' + elif impl == 'pypy': + self.ipkg_tpl = 'pypy-dbg' if dbgpkg else 'pypy' + self.ipkg_vtpl = 'pypy%s-dbg' if dbgpkg else 'pypy%s' + if impl == 'pypy': + self.ipkg_tpl_ma = self.ipkg_tpl + self.ipkg_vtpl_ma = self.ipkg_vtpl + else: + self.ipkg_tpl_ma = self.ipkg_tpl + ':any' + self.ipkg_vtpl_ma = self.ipkg_vtpl + ':any' + + self.python_dev_in_bd = 'python-dev' in bdep or\ + 'python-all-dev' in bdep or\ + 'python2-dev' in bdep or\ + 'python2-all-dev' in bdep or\ + 'python2.7-dev' in bdep or\ + 'python3-dev' in bdep or\ + 'python3-all-dev' in bdep + + self.depends = set() + self.recommends = [] + self.suggests = [] + self.enhances = [] + self.breaks = [] + self.rtscripts = [] + + def export_to(self, dh): + """Fill in debhelper's substvars.""" + prefix = PKG_PREFIX_MAP.get(self.impl, 'misc') + for i in sorted(self.depends): + dh.addsubstvar(self.package, '{}:Depends'.format(prefix), i) + for i in sorted(self.recommends): + dh.addsubstvar(self.package, '{}:Recommends'.format(prefix), i) + for i in sorted(self.suggests): + dh.addsubstvar(self.package, '{}:Suggests'.format(prefix), i) + for i in sorted(self.enhances): + dh.addsubstvar(self.package, '{}:Enhances'.format(prefix), i) + for i in sorted(self.breaks): + dh.addsubstvar(self.package, '{}:Breaks'.format(prefix), i) + for i in sorted(self.rtscripts): + dh.add_rtupdate(self.package, i) + + def __str__(self): + return "D=%s; R=%s; S=%s; E=%s, B=%s; RT=%s" %\ + (self.depends, self.recommends, self.suggests, + self.enhances, self.breaks, self.rtscripts) + + def depend(self, value): + if value and value not in self.depends: + self.depends.add(value) + + def recommend(self, value): + if value and value not in self.recommends: + self.recommends.append(value) + + def suggest(self, value): + if value and value not in self.suggests: + self.suggests.append(value) + + def enhance(self, value): + if value and value not in self.enhances: + self.enhances.append(value) + + def break_(self, value): + if value and value not in self.breaks: + self.breaks.append(value) + + def rtscript(self, value): + if value not in self.rtscripts: + self.rtscripts.append(value) + + def parse(self, stats, options): + log.debug('generating dependencies for package %s', self.package) + tpl = self.ipkg_tpl + vtpl = self.ipkg_vtpl + tpl_ma = self.ipkg_tpl_ma + vtpl_ma = self.ipkg_vtpl_ma + vrange = options.vrange + + if vrange and any((stats['compile'], stats['public_vers'], + stats['ext_vers'], stats['ext_no_version'], + stats['shebangs'])): + if any((stats['compile'], stats['public_vers'], stats['shebangs'])): + tpl_tmp = tpl_ma + else: + tpl_tmp = tpl + minv = vrange.minver + # note it's an open interval (i.e. do not add 1 here!): + maxv = vrange.maxver + if minv == maxv: + self.depend(vtpl % minv) + minv = maxv = None + if minv: + self.depend("%s (>= %s~)" % (tpl_tmp, minv)) + if maxv: + self.depend("%s (<< %s)" % (tpl_tmp, maxv)) + + if self.impl == 'cpython2' and stats['public_vers']: + # additional Depends to block python package transitions + sorted_vers = sorted(stats['public_vers']) + minv = sorted_vers[0] + maxv = sorted_vers[-1] + if minv <= default(self.impl): + self.depend("%s (>= %s~)" % (tpl_ma, minv)) + if maxv >= default(self.impl): + self.depend("%s (<< %s)" % (tpl_ma, maxv + 1)) + + if self.impl == 'pypy' and stats.get('ext_soabi'): + # TODO: make sure alternative is used only for the same extension names + # ie. for foo.ABI1.so, foo.ABI2.so, bar.ABI3,so, bar.ABI4.so generate: + # pypy-abi-ABI1 | pypy-abi-ABI2, pypy-abi-ABI3 | pypy-abi-ABI4 + self.depend('|'.join(soabi.replace('-', '-abi-') + for soabi in sorted(stats['ext_soabi']))) + + if stats['ext_vers']: + # TODO: what about extensions with stable ABI? + sorted_vers = sorted(stats['ext_vers']) + minv = sorted_vers[0] + maxv = sorted_vers[-1] + #self.depend('|'.join(vtpl % i for i in stats['ext_vers'])) + if minv <= default(self.impl): + self.depend("%s (>= %s~)" % (tpl, minv)) + if maxv >= default(self.impl): + self.depend("%s (<< %s)" % (tpl, maxv + 1)) + + # make sure py{,3}compile binary is available + if stats['compile'] and self.impl in MINPYCDEP: + self.depend(MINPYCDEP[self.impl]) + + for ipreter in stats['shebangs']: + self.depend("%s%s" % (ipreter, '' if self.impl == 'pypy' else ':any')) + + supported_versions = supported(self.impl) + default_version = default(self.impl) + for private_dir, details in stats['private_dirs'].items(): + versions = list(i.version for i in details.get('shebangs', []) if i.version and i.version.minor) + + for v in versions: + if v in supported_versions: + self.depend(vtpl_ma % v) + else: + log.info('dependency on %s (from shebang) ignored' + ' - it\'s not supported anymore', vtpl % v) + # /usr/bin/python{,3} shebang → add python{,3} to Depends + if any(True for i in details.get('shebangs', []) if i.version is None or i.version.minor is None): + self.depend(tpl_ma) + + extensions = False + if self.python_dev_in_bd: + extensions = sorted(details.get('ext_vers', set())) + #self.depend('|'.join(vtpl % i for i in extensions)) + if extensions: + self.depend("%s (>= %s~)" % (tpl, extensions[0])) + self.depend("%s (<< %s)" % (tpl, extensions[-1] + 1)) + elif details.get('ext_no_version'): + # assume unrecognized extension was built for default interpreter version + self.depend("%s (>= %s~)" % (tpl, default_version)) + self.depend("%s (<< %s)" % (tpl, default_version + 1)) + + if details.get('compile'): + if self.impl in MINPYCDEP: + self.depend(MINPYCDEP[self.impl]) + args = '' + if extensions: + args += "-V %s" % VersionRange(minver=extensions[0], maxver=extensions[-1]) + elif len(versions) == 1: # only one version from shebang + #if versions[0] in supported_versions: + args += "-V %s" % versions[0] + # ... otherwise compile with default version + elif details.get('ext_no_version'): + # assume unrecognized extension was built for default interpreter version + args += "-V %s" % default_version + elif vrange: + args += "-V %s" % vrange + if vrange.minver == vrange.maxver: + self.depend(vtpl % vrange.minver) + else: + if vrange.minver: # minimum version specified + self.depend("%s (>= %s~)" % (tpl_ma, vrange.minver)) + if vrange.maxver: # maximum version specified + self.depend("%s (<< %s)" % (tpl_ma, vrange.maxver + 1)) + + for regex in options.regexpr or []: + args += " -X '%s'" % regex.pattern.replace("'", r"'\''") + self.rtscript((private_dir, args)) + + section_options = { + 'depends_sec': options.depends_section, + 'recommends_sec': options.recommends_section, + 'suggests_sec': options.suggests_section, + } + guess_deps = partial(guess_dependency, impl=self.impl, bdep=self.bdep, + accept_upstream_versions=options.accept_upstream_versions) + if options.guess_deps: + for fn in stats['requires.txt']: + # TODO: should options.recommends and options.suggests be + # removed from requires.txt? + deps = parse_pydep(self.impl, fn, bdep=self.bdep, **section_options) + [self.depend(i) for i in deps['depends']] + [self.recommend(i) for i in deps['recommends']] + [self.suggest(i) for i in deps['suggests']] + for fpath in stats['egg-info']: + with open(fpath, 'r', encoding='utf-8') as fp: + for line in fp: + if line.startswith('Requires: '): + req = line[10:].strip() + self.depend(guess_deps(req=req)) + for fpath in stats['dist-info']: + deps = parse_requires_dist(self.impl, fpath, bdep=self.bdep, + **section_options) + [self.depend(i) for i in deps['depends']] + [self.recommend(i) for i in deps['recommends']] + [self.suggest(i) for i in deps['suggests']] + + # add dependencies from --depends + for item in options.depends or []: + self.depend(guess_deps(req=item)) + # add dependencies from --recommends + for item in options.recommends or []: + self.recommend(guess_deps(req=item)) + # add dependencies from --suggests + for item in options.suggests or []: + self.suggest(guess_deps(req=item)) + # add dependencies from --requires + for fn in options.requires or []: + fpath = join('debian', self.package, fn) + if not exists(fpath): + fpath = fn + if not exists(fpath): + log.warn('cannot find requirements file: %s', fn) + continue + deps = parse_pydep(self.impl, fpath, bdep=self.bdep, **section_options) + [self.depend(i) for i in deps['depends']] + [self.recommend(i) for i in deps['recommends']] + [self.suggest(i) for i in deps['suggests']] + + log.debug(self) |