summaryrefslogtreecommitdiffstats
path: root/dhpython/depends.py
diff options
context:
space:
mode:
Diffstat (limited to 'dhpython/depends.py')
-rw-r--r--dhpython/depends.py281
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)