# Copyright (C) 2010-2012 Aleksey Lim # # 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, either version 3 of the License, or # (at your option) any later version. # # 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, see . import re import os import logging from os.path import join, exists, dirname from ConfigParser import ConfigParser from sugar_network.toolkit.licenses import GOOD_LICENSES from sugar_network.toolkit import exception, enforce EMPTY_LICENSE = 'License is not specified' _FIELDS = { # name: (required, typecast) 'context': (True, None), 'name': (True, None), 'summary': (True, None), 'description': (False, None), 'license': (True, lambda x: _parse_list(x)), 'homepage': (True, None), 'icon': (False, None), 'version': (True, None), 'stability': (True, None), 'tags': (False, lambda x: _parse_list(x)), 'mime_types': (False, lambda x: _parse_list(x)), } _ARCHES = ['all', 'any'] _STABILITIES = ('insecure', 'buggy', 'developer', 'testing', 'stable') _POLICY_URL = 'http://wiki.sugarlabs.org/go/Sugar_Network/Policy' _LIST_SEPARATOR = ',' _RESTRICTION_RE = re.compile('(<|<=|=|==|>|>=)\\s*([0-9.]+)') _VERSION_RE = re.compile('-([a-z]*)') _VERSION_MOD_TO_VALUE = { 'pre': -2, 'rc': -1, '': 0, 'r': 1, 'post': 2, } _VERSION_VALUE_TO_MOD = {} _logger = logging.getLogger('sweets-recipe') def parse_version(version_string, ignore_errors=False): """Convert a version string to an internal representation. The parsed format can be compared quickly using the standard Python functions. Adapted Zero Install version. :param version_string: version in format supported by 0install :returns: array of arrays of integers """ if version_string is None: return None parts = _VERSION_RE.split(version_string.lower()) if parts[-1] == '': del parts[-1] # Ends with a modifier else: parts.append('') enforce(parts, ValueError, 'Empty version string') def to_int(x): pos = 0 for i in x: if not i.isdigit(): enforce(ignore_errors, ValueError, 'Only numbers are allowed in version category') break pos += 1 if pos: return int(x[:pos]) else: enforce(ignore_errors, ValueError, 'Empty version category') return 0 length = len(parts) try: for x in range(0, length, 2): part = parts[x] if part: parts[x] = [to_int(i) for i in part.split('.')] else: parts[x] = [] # (because ''.split('.') == [''], not []) except ValueError as error: exception() raise ValueError('Invalid version format in "%s": %s' % (version_string, error)) except KeyError as error: raise ValueError('Invalid version modifier in "%s": %s' % (version_string, error)) for x in range(1, length, 2): parts[x] = _VERSION_MOD_TO_VALUE[parts[x]] return parts def format_version(version): """Convert version to string representation. If string value is passed, it will be parsed to procuduce canonicalized string representation. """ if version is None: return None if isinstance(version, basestring): version = parse_version(version) if not _VERSION_VALUE_TO_MOD: for mod, value in _VERSION_MOD_TO_VALUE.items(): _VERSION_VALUE_TO_MOD[value] = mod version = version[:] length = len(version) for x in range(0, length, 2): version[x] = '.'.join([str(i) for i in version[x]]) for x in range(1, length, 2): version[x] = '-' + _VERSION_VALUE_TO_MOD[version[x]] if version[-1] == '-': del version[-1] return ''.join(version) def parse_requires(requires): result = {} for dep_str in _parse_list(requires): parts = _RESTRICTION_RE.split(dep_str) enforce(parts[0], 'Cannot parse %r dependency', dep_str) dep_name = parts.pop(0).strip() dep = result.setdefault(dep_name, []) while len(parts) >= 3: rel = parts[0] if rel in ('=', '=='): rel = [0] elif rel == '<': rel = [-1] elif rel == '>': rel = [1] elif rel == '<=': rel = [-1, 0] elif rel == '>=': rel = [1, 0] dep.append((rel, parse_version(parts[1]))) del parts[:3] enforce(not parts or not parts[0].strip(), 'Cannot parse %r dependency', dep_str) return result def ensure(version, cond): if cond: for op, cond_version in cond: if op == [0]: # Make `version` the same length as `cond_version` if len(version) > len(cond_version): version = version[:len(cond_version) - 1] + [0] if len(version[0]) > len(cond_version[0]): version = [version[0][:len(cond_version[0])], 0] if cmp(version, cond_version) not in op: return False return True class Spec(object): def __init__(self, spec=None, root=None): self.path = None self.command = None self.bindings = set() self.requires = {} self.build_requires = [] self.source_requires = [] self.archives = [] self.applications = [] self.activity = None self.library = None self._fields = {} self._noarch = True self._config = ConfigParser() if hasattr(spec, 'readline'): self._config.readfp(spec) else: if spec is not None: enforce(exists(spec), 'Recipe file %s does not exist', spec) self.path = spec elif root is not None: # TODO Handle sweets.recipe specs self.path = join(root, 'activity', 'activity.info') self._config.read(self.path) self._read() @property def root(self): if self.path is not None: return dirname(dirname(self.path)) @property def types(self): result = [] if self.activity is not None: result.append('activity') if self.library is not None: result.append('library') if self.applications: result.append('application') return result @property def noarch(self): return self._noarch def lint(self, is_sweet=None): for i in self['license']: enforce(i in GOOD_LICENSES, 'Not supported "%s" license, see %s for details', i, _POLICY_URL) def __getitem__(self, key): section = None if isinstance(key, tuple): if len(key) == 2: section, key = key else: enforce(len(key) == 1) key = key[0] if not section: if key in _FIELDS: return self._fields.get(key) section = 'DEFAULT' return self._get(section, key) def __repr__(self): return '' % self['context'] def _get(self, section, key): if self._config.has_option(section, key): return self._config.get(section, key) def _read(self): for section in sorted(self._config.sections()): bindings = _parse_bindings(self._get(section, 'binding')) self.bindings.update(bindings) requires = parse_requires(self._get(section, 'requires')) section_type = section.split(':')[0] if section_type == 'Activity': self._new_activity(section, requires) elif section_type == 'Application': cmd_name = section.split(':')[-1] if ':' in section else 'run' self._new_command(section, requires, cmd_name) self.applications.append(_Application(self._config, section)) elif section_type == 'Library': enforce(':' not in section, '[Library] should be singular') enforce(self._get(section, 'binding'), 'Option "binding" should exist') self.library = _Library(self._config, section) self.requires.update(requires) elif section_type == 'Package': self.requires.update(requires) elif section_type == 'Build': for i in requires: i.for_build = True self.build_requires.extend(requires) continue elif section_type == 'Source': self.source_requires.extend(requires) continue else: if section_type == 'Archive': self._new_archive(section) # The further code only for usecase sections continue for key, (__, typecast) in _FIELDS.items(): value = self._get(section, key) if value is None: continue enforce(self._fields.get(key) is None or self._fields[key] == value, 'Option %s should be the same for all sections', key) if typecast is not None: value = typecast(value) self._fields[key] = value if self.activity is not None: # TODO Switch to `context` tag at the end self._fields['context'] = self.activity['bundle_id'] # Do some backwards compatibility expansions for activities if not self['summary'] and self['name']: self._fields['summary'] = self['name'] if not self['version'] and self.activity['activity_version']: self._fields['version'] = self.activity['activity_version'] if not self['stability']: self._fields['stability'] = 'stable' if not self['homepage']: self._fields['homepage'] = \ 'http://wiki.sugarlabs.org/go/Activities/%s' % \ self['name'] if not self['icon'].lower().endswith('.svg'): self._fields['icon'] = join('activity', self['icon'] + '.svg') if not self['license']: self._fields['license'] = EMPTY_LICENSE for key, (required, __) in _FIELDS.items(): enforce(not required or key in self._fields, 'Option "%s" is required', key) if 'Build' in self._config.sections(): enforce(self._get('Build', 'install'), 'At least "install" should exists in [Build]') if not self['description']: self._fields['description'] = self['summary'] self._fields['version'] = format_version(self['version']) if not self.archives: self.archives.append(_Archive(self._config, 'DEFAULT')) for i in self.applications: i.name = '-'.join(i.section.split(':')[1:]) def _new_command(self, section, requires, name): enforce(self.command is None, 'Only one command is allowed') cmdline = self._get(section, 'exec') enforce(cmdline, 'Option "exec" should exist for [%s] section', section) self.command = cmdline self.requires.update(requires) def _new_activity(self, section, requires): enforce(':' not in section, '[Activity] should be singular') # Support deprecated activity.info options for new_key, old_key, fmt in [ ('exec', 'class', 'sugar-activity %s'), ('bundle_id', 'service_name', '%s')]: if not self._get(section, new_key) and self._get(section, old_key): _logger.warning('Option "%s" is deprecated, use "%s" instead', old_key, new_key) self._config.set(section, new_key, fmt % self._get(section, old_key)) for key in ['icon', 'exec']: enforce(self._get(section, key), 'Option "%s" should exist for activities', key) self._new_command(section, requires, 'activity') self.activity = _Activity(self._config, section) def _new_archive(self, section): arch = self._get(section, 'arch') or 'all' enforce(arch in _ARCHES, 'Unknown arch %s in [%s], it should be %r', arch, section, _ARCHES) self._noarch = self._noarch and (arch == 'all') if self._get(section, 'lang'): assert False, 'Not yet implemented' """ for lang in get_list('langs', section): result.add(SubPackage(section, lang)) """ else: self.archives.append(_Archive(self._config, section)) class _Section(object): def __init__(self, config, section): self._config = config self.section = section self.name = None def __getitem__(self, key): return self._config.get(self.section, key) class _Archive(_Section): @property def include(self): # TODO per lang includes return _parse_list(self['include']) @property def exclude(self): # TODO per lang excludes return _parse_list(self['exclude']) @property def noarch(self): return (self['arch'] or 'all') == 'all' class _Application(_Section): pass class _Activity(_Section): pass class _Library(_Section): pass def _parse_bindings(text): result = set() def parse_str(bind_str): parts = bind_str.split() if not parts: return if parts[0].lower() in ['prepend', 'append', 'replace']: mode = parts.pop(0).lower() else: mode = 'prepend' if len(parts) > 1: insert = parts.pop().strip(os.sep) else: insert = '' result.add((parts[0], insert, mode)) for i in _parse_list(text): parse_str(i) return sorted(result) def _parse_list(str_list): if not str_list: return [] parts = [] brackets = {('(', ')'): 0, ('[', ']'): 0, ('"', '"'): 0} str_list = str_list.replace("\n", _LIST_SEPARATOR).strip() i = 0 while i < len(str_list): if not max(brackets.values()) and \ str_list[i] in (_LIST_SEPARATOR, ';'): parts.append(str_list[:i]) str_list = str_list[i + 1:] i = 0 else: for key in brackets.keys(): left, right = key if str_list[i] == left: brackets[key] += 1 break elif str_list[i] == right: brackets[key] -= 1 break i += 1 parts.append(str_list) return [i.strip() for i in parts if i.strip()]