# -*- coding: UTF-8 -*- # Copyright © 2010 Piotr Ożarowski # # 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. from __future__ import with_statement import logging import os import re from os.path import exists, isdir, join from subprocess import PIPE, Popen from debpython.version import vrepr, getver, get_requested_versions from debpython.tools import memoize log = logging.getLogger(__name__) PUBLIC_DIR_RE = re.compile(r'.*?/usr/lib/python(\d.\d+)/(site|dist)-packages') PYDIST_RE = re.compile(r""" (?P[A-Za-z][A-Za-z0-9_.]*) # Python distribution name \s* (?P(?:-?\d\.\d+(?:-(?:\d\.\d+)?)?)?) # version range \s* (?P(?:[a-z][^;]*)?) # Debian dependency (?: # optional upstream version -> Debian version translator ;\s* (?PPEP386)? # PEP-386 mode \s* (?Ps/.*)? # translator rules )? """, re.VERBOSE) REQUIRES_RE = re.compile(r''' (?P[A-Za-z][A-Za-z0-9_.]*) # Python distribution name \s* (?P(?:\[[^\]]*\])?) # ignored for now \s* (?: # optional minimum/maximum version (?P<=?|>=?|==|!=) \s* (?P(\w|[-.])+) )? ''', re.VERBOSE) def validate(fpath, exit_on_error=False): """Check if pydist file looks good.""" with open(fpath) as fp: for line in fp: line = line.strip('\r\n') if line.startswith('#') or not line: continue if not PYDIST_RE.match(line): log.error('invalid pydist data in file %s: %s', \ fpath.rsplit('/', 1)[-1], line) if exit_on_error: exit(3) return False return True @memoize def load(dname='/usr/share/python/dist/', fname='debian/pydist-overrides', fbname='/usr/share/python/dist_fallback'): """Load iformation about installed Python distributions.""" if exists(fname): to_check = [fname] # first one! else: to_check = [] if isdir(dname): to_check.extend(join(dname, i) for i in os.listdir(dname)) if exists(fbname): # fall back generated at python-defaults build time to_check.append(fbname) # last one! result = {} for fpath in to_check: with open(fpath) as fp: for line in fp: line = line.strip('\r\n') if line.startswith('#') or not line: continue dist = PYDIST_RE.search(line) if not dist: log.error('%s file has a broken line: %s', fpath, line) exit(9) dist = dist.groupdict() name = safe_name(dist['name']) dist['versions'] = get_requested_versions(dist['vrange']) dist['dependency'] = dist['dependency'].strip() if dist['rules']: dist['rules'] = dist['rules'].split(';') else: dist['rules'] = [] result.setdefault(name, []).append(dist) return result def guess_dependency(req, version=None): log.debug('trying to guess dependency for %s (python=%s)', req, vrepr(version) if version else None) if isinstance(version, basestring): version = getver(version) # some upstreams have weird ideas for distribution name... name, rest = re.compile('([^><= ]+)(.*)').match(req).groups() req = safe_name(name) + rest data = load() req_dict = REQUIRES_RE.match(req) if not req_dict: log.error('requirement is not valid: %s', req) log.info('please ask dh_python2 author to fix REQUIRES_RE ' 'or your upstream author to fix requires.txt') exit(8) req_dict = req_dict.groupdict() details = data.get(req_dict['name'].lower()) if details: for item in details: if version and version not in item.get('versions', version): # rule doesn't match version, try next one continue if not item['dependency']: return # this requirement should be ignored if item['dependency'].endswith(')'): # no need to translate versions if version is hardcoded in Debian # dependency return item['dependency'] if req_dict['version']: # FIXME: translate it (rules, versions) return item['dependency'] else: return item['dependency'] # try dpkg -S query = "'%s-?*\.egg-info'" % safe_name(name) # TODO: .dist-info if version: query = "%s | grep '/python%s/\|/pyshared/'" % \ (query, vrepr(version)) else: query = "%s | grep '/python2\../\|/pyshared/'" % query log.debug("invoking dpkg -S %s", query) process = Popen("/usr/bin/dpkg -S %s" % query, \ shell=True, stdout=PIPE) stdout, stderr = process.communicate() if process.returncode == 0: result = set() for line in stdout.split('\n'): if not line.strip(): continue result.add(line.split(':')[0]) if len(result) > 1: log.error('more than one package name found for %s dist', name) else: return result.pop() # fall back to python-distname pname = sensible_pname(name) log.warn('Cannot find package that provides %s. ' 'Using %s as package name. Please add "%s correct_package_name" ' 'line to debian/pydist-overrides to override it.', name, pname, name) return pname def parse_pydep(fname): public_dir = PUBLIC_DIR_RE.match(fname) if public_dir: ver = public_dir.group(1) else: ver = None result = [] with open(fname, 'r') as fp: for line in fp: line = line.strip() # ignore all optional sections if line.startswith('['): break if line: dependency = guess_dependency(line, ver) if dependency: result.append(dependency) return result def safe_name(name): """Emulate distribute's safe_name.""" return re.compile('[^A-Za-z0-9.]+').sub('_', name).lower() def sensible_pname(egg_name): """Guess Debian package name from Egg name.""" egg_name = safe_name(egg_name).replace('_', '-') if egg_name.startswith('python-'): egg_name = egg_name[7:] return "python-%s" % egg_name.lower()