# 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 sys
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 format_next_version(version, deep=True):
"""Convert incremented version to string representation.
Before convertation, the last version's rank will be incremented.
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 deep:
version[-2] += [1]
else:
version[-2][-1] += 1
return format_version(version)
def parse_requires(requires):
result = {}
for dep_str in _parse_list(requires):
dep = _Dependency()
if dep_str.startswith('[') and dep_str.endswith(']'):
dep_str = dep_str[1:-1]
dep['importance'] = 'recommended'
parts = _RESTRICTION_RE.split(dep_str)
enforce(parts[0], 'Can parse dependency from "%s" string', dep_str)
dep_name = parts.pop(0).strip()
if dep_name in result:
result[dep_name].update(dep)
dep = result[dep_name]
else:
result[dep_name] = dep
not_before = None
before = None
while len(parts) >= 3:
if parts[0] == '<':
before = format_version(parts[1])
elif parts[0] == '<=':
before = format_next_version(parts[1], False)
elif parts[0] == '>':
not_before = format_next_version(parts[1], False)
elif parts[0] == '>=':
not_before = format_version(parts[1])
elif parts[0] == '=':
not_before = format_version(parts[1])
before = format_next_version(parts[1], False)
del parts[:3]
enforce(not parts or not parts[0].strip(),
'Cannot parse "%s", it should be in format '
'" (>=|<|=) "', dep_str)
if before or not_before:
dep.setdefault('restrictions', [])
dep['restrictions'].append((not_before, before))
return result
def ensure_requires(to_consider, to_apply):
def intersect(x, y):
l = max([parse_version(i) for i, __ in (x + y)])
r = min([[[sys.maxint]] if i is None else parse_version(i)
for __, i in (x + y)])
return l is None or r is None or l < r
for name, cond in to_apply.items():
dep = to_consider.get(name)
if dep is None:
return False
if 'restrictions' not in dep or 'restrictions' not in cond:
continue
if not intersect(dep['restrictions'], cond['restrictions']):
return False
return True
class Spec(object):
def __init__(self, spec=None, root=None):
self.path = None
self.commands = {}
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):
cmdline = self._get(section, 'exec')
enforce(cmdline,
'Option "exec" should exist for [%s] section', section)
command = self.commands[name] = _Command(name, cmdline)
if ':' in section:
command.requires.update(requires)
else:
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
class _Command(dict):
def __init__(self, name, cmdline):
dict.__init__(self)
self['exec'] = cmdline
self.name = name
self.requires = {}
class _Dependency(dict):
def versions_range(self):
for not_before, before in self.get('restrictions') or []:
if not_before is None:
continue
i = parse_version(not_before)[0]
yield format_version([i, 0])
if before is None:
continue
end = parse_version(before)[0]
i = i[:min(len(i), len(end))]
while True:
i[-1] += 1
if i >= end:
break
yield format_version([i, 0])
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()]