# Copyright © 2010-2013 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. import errno import logging import re from os import makedirs, chmod, environ from os.path import basename, exists, join, dirname from sys import argv from dhpython import DEPENDS_SUBSTVARS, PKG_NAME_TPLS, RT_LOCATIONS, RT_TPLS log = logging.getLogger('dhpython') parse_dep = re.compile('''[,\s]* (?P[^ :]+)(?::any)? \s* \(?(?P([>=<]{2,}|=)\s*[^\)]+)?\)? \s* (?:\[(?P[^\]]+)\])? ''', re.VERBOSE).match def build_options(**options): """Build an Options object from kw options""" default_options = { 'arch': None, 'package': [], 'no_package': [], 'write_log': False, 'remaining_packages': False, } built_options = default_options built_options.update(options) return type('Options', (object,), built_options) class DebHelper: """Reinvents the wheel / some dh functionality (Perl is ugly ;-P)""" def __init__(self, options, impl='cpython3'): self.options = options self.packages = {} self.build_depends = {} self.python_version = None # Note that each DebHelper instance supports ONE interpreter type only # it's not possible to mix cpython2, cpython3 and pypy here self.impl = impl self.command = { 'cpython2': 'dh_python2', 'cpython3': 'dh_python3', 'pypy': 'dh_pypy', }[impl] skip_tpl = set() for name, tpls in PKG_NAME_TPLS.items(): if name != impl: skip_tpl.update(tpls) skip_tpl = tuple(skip_tpl) substvar = DEPENDS_SUBSTVARS[impl] pkgs = options.package skip_pkgs = options.no_package try: with open('debian/control', 'r', encoding='utf-8') as fp: paragraphs = [{}] field = None for lineno, line in enumerate(fp, 1): if line.startswith('#'): continue if not line.strip(): if paragraphs[-1]: paragraphs.append({}) field = None continue if line[0].isspace(): # Continuation paragraphs[-1][field] += line.rstrip() continue if not ':' in line: raise Exception( 'Unable to parse line %i in debian/control: %s' % (lineno, line)) field, value = line.split(':', 1) field = field.lower() paragraphs[-1][field] = value.strip() except IOError: raise Exception('cannot find debian/control file') # Trailing new lines? if not paragraphs[-1]: paragraphs.pop() if len(paragraphs) < 2: raise Exception('Unable to parse debian/control, found less than ' '2 paragraphs') self.source_name = paragraphs[0]['source'] if self.impl == 'cpython3' and 'x-python3-version' in paragraphs[0]: self.python_version = paragraphs[0]['x-python3-version'] if len(self.python_version.split(',')) > 2: raise ValueError('too many arguments provided for ' 'X-Python3-Version: min and max only.') elif self.impl == 'cpython2': if 'x-python-version' in paragraphs[0]: self.python_version = paragraphs[0]['x-python-version'] elif 'xs-python-version' in paragraphs[0]: self.python_version = paragraphs[0]['xs-python-version'] build_depends = [] for field in ('build-depends', 'build-depends-indep', 'build-depends-arch'): if field in paragraphs[0]: build_depends.append(paragraphs[0][field]) build_depends = ', '.join(build_depends) for dep1 in build_depends.split(','): for dep2 in dep1.split('|'): details = parse_dep(dep2) if details: details = details.groupdict() if details['arch']: architectures = details['arch'].split() else: architectures = [None] for arch in architectures: self.build_depends.setdefault( details['name'], {})[arch] = details['version'] for paragraph_no, paragraph in enumerate(paragraphs[1:], 2): if 'package' not in paragraph: raise Exception('Unable to parse debian/control, paragraph %i ' 'missing Package field' % paragraph_no) binary_package = paragraph['package'] if skip_tpl and binary_package.startswith(skip_tpl): log.debug('skipping package: %s', binary_package) continue if pkgs and binary_package not in pkgs: continue if skip_pkgs and binary_package in skip_pkgs: continue if (options.remaining_packages and self.has_acted_on_package(binary_package)): continue pkg = { 'substvars': {}, 'autoscripts': {}, 'rtupdates': [], 'arch': paragraph['architecture'], } if (options.arch is False and pkg['arch'] != 'all' or options.arch is True and pkg['arch'] == 'all'): # TODO: check also if arch matches current architecture: continue if not binary_package.startswith(PKG_NAME_TPLS[impl]): # package doesn't have common prefix (python-, python3-, pypy-) # so lets check if Depends/Recommends contains the # appropriate substvar if (substvar not in paragraph.get('depends', '') and substvar not in paragraph.get('recommends', '')): log.debug('skipping package %s (missing %s in ' 'Depends/Recommends)', binary_package, substvar) continue # Operate on binary_package self.packages[binary_package] = pkg fp.close() log.debug('source=%s, binary packages=%s', self.source_name, list(self.packages.keys())) def has_acted_on_package(self, package): try: with open('debian/{}.debhelper.log'.format(package), encoding='utf-8') as f: for line in f: if line.strip() == self.command: return True except IOError as e: if e.errno != errno.ENOENT: raise return False def addsubstvar(self, package, name, value): """debhelper's addsubstvar""" self.packages[package]['substvars'].setdefault(name, []).append(value) def autoscript(self, package, when, template, args): """debhelper's autoscript""" self.packages[package]['autoscripts'].setdefault(when, {})\ .setdefault(template, []).append(args) def add_rtupdate(self, package, value): self.packages[package]['rtupdates'].append(value) def save_autoscripts(self): for package, settings in self.packages.items(): autoscripts = settings.get('autoscripts') if not autoscripts: continue for when, templates in autoscripts.items(): fn = "debian/%s.%s.debhelper" % (package, when) if exists(fn): with open(fn, 'r', encoding='utf-8') as datafile: data = datafile.read() else: data = '' new_data = '' for tpl_name, args in templates.items(): for i in args: # try local one first (useful while testing dh_python3) fpath = join(dirname(__file__), '..', "autoscripts/%s" % tpl_name) if not exists(fpath): fpath = "/usr/share/debhelper/autoscripts/%s" % tpl_name with open(fpath, 'r', encoding='utf-8') as tplfile: tpl = tplfile.read() if self.options.compile_all and args: # TODO: should args be checked to contain dir name? tpl = tpl.replace('-p #PACKAGE#', '') elif settings['arch'] == 'all': tpl = tpl.replace('#PACKAGE#', package) else: arch = environ['DEB_HOST_ARCH'] tpl = tpl.replace('#PACKAGE#', '%s:%s' % (package, arch)) tpl = tpl.replace('#ARGS#', i) if tpl not in data and tpl not in new_data: new_data += "\n%s" % tpl if new_data: data += '\n# Automatically added by {}'.format(basename(argv[0])) +\ '{}\n# End automatically added section\n'.format(new_data) fp = open(fn, 'w', encoding='utf-8') fp.write(data) fp.close() def save_substvars(self): for package, settings in self.packages.items(): substvars = settings.get('substvars') if not substvars: continue fn = "debian/%s.substvars" % package if exists(fn): with open(fn, 'r', encoding='utf-8') as datafile: data = datafile.read() else: data = '' for name, values in substvars.items(): p = data.find("%s=" % name) if p > -1: # parse the line and remove it from data e = data[p:].find('\n') line = data[p + len("%s=" % name): p + e if e > -1 else None] items = [i.strip() for i in line.split(',') if i] if e > -1 and data[p + e:].strip(): data = "%s\n%s" % (data[:p], data[p + e:]) else: data = data[:p] else: items = [] for j in values: if j not in items: items.append(j) if items: if data: data += '\n' data += "%s=%s\n" % (name, ', '.join(items)) data = data.replace('\n\n', '\n') if data: fp = open(fn, 'w', encoding='utf-8') fp.write(data) fp.close() def save_rtupdate(self): for package, settings in self.packages.items(): pkg_arg = '' if self.options.compile_all else "-p %s" % package values = settings.get('rtupdates') if not values: continue d = 'debian/{}/{}'.format(package, RT_LOCATIONS[self.impl]) if not exists(d): makedirs(d) fn = "%s/%s.rtupdate" % (d, package) if exists(fn): data = open(fn, 'r', encoding='utf-8').read() else: data = "#! /bin/sh\nset -e" for dname, args in values: cmd = RT_TPLS[self.impl].format(pkg_arg=pkg_arg, dname=dname, args=args) if cmd not in data: data += "\n%s" % cmd if data: fp = open(fn, 'w', encoding='utf-8') fp.write(data) fp.close() chmod(fn, 0o755) def save_log(self): if not self.options.write_log: return for package, settings in self.packages.items(): with open('debian/{}.debhelper.log'.format(package), 'a', encoding='utf-8') as f: f.write(self.command + '\n') def save(self): self.save_substvars() self.save_autoscripts() self.save_rtupdate() self.save_log()