diff options
author | Aleksey Lim <alsroot@sugarlabs.org> | 2012-09-22 07:11:44 (GMT) |
---|---|---|
committer | Aleksey Lim <alsroot@sugarlabs.org> | 2012-09-22 07:11:44 (GMT) |
commit | dfc1575a588c061a896c543452b9b75c4bbe1389 (patch) | |
tree | f139a8c92669e1ab3b9b2cc8a54c5c3377b8452d | |
parent | 046be81cb435826abbecff2cfb08b217ee742179 (diff) |
Merge sweets-recipe project
-rwxr-xr-x | misc/aslo_sync.py | 2 | ||||
-rw-r--r-- | sugar_network/local/activities.py | 2 | ||||
-rw-r--r-- | sugar_network/local/mounts.py | 5 | ||||
-rw-r--r-- | sugar_network/resources/implementation.py | 2 | ||||
-rw-r--r-- | sugar_network/toolkit/http.py | 2 | ||||
-rw-r--r-- | sugar_network/zerosugar/__init__.py | 4 | ||||
-rw-r--r-- | sugar_network/zerosugar/bundle.py | 124 | ||||
-rw-r--r-- | sugar_network/zerosugar/feeds.py | 11 | ||||
-rw-r--r-- | sugar_network/zerosugar/injector.py | 3 | ||||
-rw-r--r-- | sugar_network/zerosugar/licenses.py | 255 | ||||
-rw-r--r-- | sugar_network/zerosugar/spec.py | 474 | ||||
-rw-r--r-- | sweets.recipe | 4 | ||||
-rw-r--r-- | tests/units/__main__.py | 1 | ||||
-rwxr-xr-x | tests/units/spec.py | 102 |
14 files changed, 974 insertions, 17 deletions
diff --git a/misc/aslo_sync.py b/misc/aslo_sync.py index b84c841..b48e0ae 100755 --- a/misc/aslo_sync.py +++ b/misc/aslo_sync.py @@ -29,7 +29,7 @@ from os.path import join, exists import MySQLdb as mdb import active_document as ad -from sweets_recipe import GOOD_LICENSES, Bundle +from sugar_network.zerosugar import GOOD_LICENSES, Bundle from sugar_network.resources.volume import Volume diff --git a/sugar_network/local/activities.py b/sugar_network/local/activities.py index 4e488d4..8b1e052 100644 --- a/sugar_network/local/activities.py +++ b/sugar_network/local/activities.py @@ -20,7 +20,7 @@ import tempfile from os.path import join, exists, lexists, relpath, dirname, basename, isdir from os.path import abspath, islink -from sweets_recipe import Spec +from sugar_network.zerosugar import Spec from sugar_network.toolkit.inotify import Inotify, \ IN_DELETE_SELF, IN_CREATE, IN_DELETE, IN_CLOSE_WRITE, \ IN_MOVED_TO, IN_MOVED_FROM diff --git a/sugar_network/local/mounts.py b/sugar_network/local/mounts.py index d826afe..30a2172 100644 --- a/sugar_network/local/mounts.py +++ b/sugar_network/local/mounts.py @@ -19,9 +19,8 @@ import logging from os.path import isabs, exists, join, basename, isdir from gettext import gettext as _ -import sweets_recipe import active_document as ad -from sweets_recipe import Bundle +from sugar_network.zerosugar import Bundle, Spec from sugar_network.toolkit import sugar, http from sugar_network.local import activities, cache from sugar_network.resources.volume import Request @@ -130,7 +129,7 @@ class HomeMount(LocalMount): for path in activities.checkins(request['guid']): try: - spec = sweets_recipe.Spec(root=path) + spec = Spec(root=path) except Exception: util.exception(_logger, 'Failed to read %r spec file', path) continue diff --git a/sugar_network/resources/implementation.py b/sugar_network/resources/implementation.py index cbaa4de..1a6c3f1 100644 --- a/sugar_network/resources/implementation.py +++ b/sugar_network/resources/implementation.py @@ -16,7 +16,7 @@ # pylint: disable-msg=E1101,E0102,E0202 import active_document as ad -from sweets_recipe import GOOD_LICENSES +from sugar_network.zerosugar import GOOD_LICENSES from sugar_network import resources from sugar_network.resources.volume import Resource diff --git a/sugar_network/toolkit/http.py b/sugar_network/toolkit/http.py index 860a7d9..5e37acb 100644 --- a/sugar_network/toolkit/http.py +++ b/sugar_network/toolkit/http.py @@ -29,7 +29,7 @@ from requests.sessions import Session from M2Crypto import DSA import active_document as ad -from sweets_recipe import Bundle +from sugar_network.zerosugar import Bundle from active_toolkit.sockets import decode_multipart, BUFFER_SIZE from sugar_network.toolkit import sugar from sugar_network import local diff --git a/sugar_network/zerosugar/__init__.py b/sugar_network/zerosugar/__init__.py index 966170e..f7993dd 100644 --- a/sugar_network/zerosugar/__init__.py +++ b/sugar_network/zerosugar/__init__.py @@ -17,6 +17,10 @@ import sys from os.path import join, abspath, dirname sys.path.insert(0, join(abspath(dirname(__file__)), 'lib')) +from .spec import Spec, parse_version, format_version +from .bundle import Bundle, BundleError +from .licenses import GOOD_LICENSES + def _init(): from zeroinstall.injector import reader, model diff --git a/sugar_network/zerosugar/bundle.py b/sugar_network/zerosugar/bundle.py new file mode 100644 index 0000000..06cee45 --- /dev/null +++ b/sugar_network/zerosugar/bundle.py @@ -0,0 +1,124 @@ +# 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 <http://www.gnu.org/licenses/>. + +import os +import tarfile +import zipfile +from os.path import join + +from sugar_network.zerosugar.spec import Spec + + +class BundleError(Exception): + pass + + +class Bundle(object): + + def __init__(self, bundle, mime_type=None): + self._extract = False + + if mime_type is None: + mime_type = _detect_mime_type(bundle) or '' + + if mime_type == 'application/zip': + self._bundle = zipfile.ZipFile(bundle) + self._do_get_names = self._bundle.namelist + self._do_extractfile = self._bundle.open + elif mime_type.split('/')[-1].endswith('-tar'): + self._bundle = tarfile.open(bundle) + self._do_get_names = self._bundle.getnames + self._do_extractfile = self._bundle.extractfile + else: + raise BundleError('Unsupported bundle type for "%s" file, ' + 'it can be either tar or zip.' % bundle) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self._bundle.close() + self._bundle = None + + def get_names(self): + return self._do_get_names() + + def extractfileto(self, name, dst_path): + f = file(dst_path, 'w') + f.write(self._do_extractfile(name).read()) + f.close() + + def extractfile(self, name): + return self._do_extractfile(name) + + def extractall(self, path, members=None): + self._bundle.extractall(path=path, members=members) + + @property + def extract(self): + if self._extract is not False: + return self._extract + self._extract = None + + for arcname in self.get_names(): + parts = arcname.split(os.sep) + if len(parts) > 1: + if self._extract is None: + self._extract = parts[0] + elif parts[0] != self._extract: + self._extract = None + break + + return self._extract + + def get_spec(self): + if self.extract: + specs = (join(self.extract, 'sweets.recipe'), + join(self.extract, 'activity', 'activity.info')) + else: + specs = ('sweets.recipe', join('activity', 'activity.info')) + + for arcname in self.get_names(): + if arcname in specs: + f = self.extractfile(arcname) + try: + return Spec(f) + finally: + f.close() + + +def _detect_mime_type(filename): + if filename.endswith('.xo'): + return 'application/zip' + if filename.endswith('.zip'): + return 'application/zip' + if filename.endswith('.tar.bz2'): + return 'application/x-bzip-compressed-tar' + if filename.endswith('.tar.gz'): + return 'application/x-compressed-tar' + if filename.endswith('.tar.lzma'): + return 'application/x-lzma-compressed-tar' + if filename.endswith('.tar.xz'): + return 'application/x-xz-compressed-tar' + if filename.endswith('.tbz'): + return 'application/x-bzip-compressed-tar' + if filename.endswith('.tgz'): + return 'application/x-compressed-tar' + if filename.endswith('.tlz'): + return 'application/x-lzma-compressed-tar' + if filename.endswith('.txz'): + return 'application/x-xz-compressed-tar' + if filename.endswith('.tar'): + return 'application/x-tar' diff --git a/sugar_network/zerosugar/feeds.py b/sugar_network/zerosugar/feeds.py index 8cc60ac..675d4ac 100644 --- a/sugar_network/zerosugar/feeds.py +++ b/sugar_network/zerosugar/feeds.py @@ -19,8 +19,7 @@ from os.path import isabs from zeroinstall.injector import model -import sweets_recipe -from sugar_network.zerosugar import lsb_release +from sugar_network.zerosugar import lsb_release, parse_version from active_toolkit import util, enforce @@ -67,7 +66,7 @@ def read(context): impl = _Implementation(feed, impl_id, None) impl.client = client - impl.version = sweets_recipe.parse_version(version) + impl.version = parse_version(version) impl.released = 0 impl.arch = arch impl.upstream_stability = \ @@ -140,7 +139,7 @@ class _Feed(model.ZeroInstallFeed): top_package = packages[0] impl = _Implementation(self, self.context, None) - impl.version = sweets_recipe.parse_version(top_package['version']) + impl.version = parse_version(top_package['version']) impl.released = 0 impl.arch = '*-%s' % top_package['arch'] impl.upstream_stability = model.stability_levels['packaged'] @@ -171,8 +170,8 @@ class _Dependency(model.InterfaceDependency): for not_before, before in data.get('restrictions') or []: restriction = model.VersionRangeRestriction( - not_before=sweets_recipe.parse_version(not_before), - before=sweets_recipe.parse_version(before)) + not_before=parse_version(not_before), + before=parse_version(before)) self.restrictions.append(restriction) @property diff --git a/sugar_network/zerosugar/injector.py b/sugar_network/zerosugar/injector.py index df30dc9..f4b6adc 100644 --- a/sugar_network/zerosugar/injector.py +++ b/sugar_network/zerosugar/injector.py @@ -21,8 +21,7 @@ from os.path import join, exists, basename from zeroinstall.injector import model from zeroinstall.injector.requirements import Requirements -from sweets_recipe import Spec -from sugar_network.zerosugar import pipe, packagekit +from sugar_network.zerosugar import pipe, packagekit, Spec from sugar_network.zerosugar.solution import solve from sugar_network import local from sugar_network.toolkit import sugar diff --git a/sugar_network/zerosugar/licenses.py b/sugar_network/zerosugar/licenses.py new file mode 100644 index 0000000..73dec75 --- /dev/null +++ b/sugar_network/zerosugar/licenses.py @@ -0,0 +1,255 @@ +# Copyright (C) 2011-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 <http://www.gnu.org/licenses/>. + +# This list is based on Good Licenses from +# http://fedoraproject.org/wiki/Licensing:Main + + +GOOD_LICENSES = frozenset([ + # Software + 'Glide', + 'Abstyles', + 'AFL', + 'AMPAS BSD', + 'Adobe', + 'MIT', + 'AGPLv1', + 'AGPLv3', + 'AGPLv3+', + 'AGPLv3 with exceptions', + 'ADSL', + 'AMDPLPA', + 'ASL 1.0', + 'ASL 1.1', + 'ASL 2.0', + 'AML', + 'APSL 2.0', + 'Artistic clarified', + 'Artistic 2.0', + 'ARL', + 'AAL', + 'Bahyph', + 'Barr', + 'BeOpen', + 'Bibtex', + 'BitTorrent', + 'Boost', + 'Borceux', + 'BSD with advertising', + 'BSD with attribution', + 'BSD', + 'BSD Protection', + 'CATOSL', + 'CeCILL', + 'CeCILL-B', + 'CeCILL-C', + 'Netscape', + 'CNRI', + 'CDDL', + 'CPL', + 'Condor', + 'Copyright only', + 'CPAL', + 'GPLv2+', + 'LGPLv2+', + 'CC0', + 'Crossword', + 'Crystal Stacker', + 'diffmark', + 'WTFPL', + 'DOC', + 'Dotseqn', + 'dvipdfm', + 'EPL', + 'eCos', + 'ECL 1.0', + 'ECL 2.0', + 'EFL 2.0', + 'MIT with advertising', + 'Entessa', + 'ERPL', + 'EU Datagrid', + 'EUPL 1.1', + 'Eurosym', + 'Fair', + 'FTL', + 'Giftware', + 'GL2PS', + 'GPL+', + 'GPL+ with exceptions', + 'GPLv1', + 'GPLv2', + 'GPLv2 with exceptions', + 'GPLv2+ with exceptions', + 'GPLv3', + 'GPLv3 with exceptions', + 'GPLv3+', + 'GPLv3+ with exceptions', + 'LGPLv2', + 'LGPLv2 with exceptions', + 'LGPLv2+ with exceptions', + 'LGPLv3', + 'LGPLv3 with exceptions', + 'LGPLv3+', + 'LGPLv3+ with exceptions', + 'gnuplot', + 'HaskellReport', + 'IBM', + 'iMatix', + 'ImageMagick', + 'Imlib2', + 'IJG', + 'Intel ACPI', + 'Interbase', + 'ISC', + 'Jabber', + 'JasPer', + 'JPython', + 'Knuth', + 'LPPL', + 'Latex2e', + 'LBNL BSD', + 'Lhcyr', + 'libtiff', + 'LLGPL', + 'Logica', + 'LPL', + 'MakeIndex', + 'mecab-ipadic', + 'MS-PL', + 'MS-RL', + 'MirOS', + 'mod_macro', + 'Motosoto', + 'MPLv1.0', + 'MPLv1.1', + 'Naumen', + 'NCSA', + 'NetCDF', + 'NGPL', + 'NOSL', + 'Newmat', + 'Newsletr', + 'Nokia', + 'Noweb', + 'OpenLDAP', + 'OML', + 'OpenPBS', + 'OSL 1.0', + 'OSL 1.1', + 'OSL 2.0', + 'OSL 2.1', + 'OSL 3.0', + 'OpenSSL', + 'OReilly', + 'GPL+ or Artistic', + 'GPLv2 or Artistic', + 'GPLv2+ or Artistic', + 'LGPLv2+ or Artistic', + 'Phorum', + 'PHP', + 'PlainTeX', + 'Plexus', + 'PostgreSQL', + 'psfrag', + 'psutils', + 'Public Domain', + 'Python', + 'Qhull', + 'QPL', + 'Rdisc', + 'RPSL', + 'RiceBSD', + 'Romio', + 'Rsfs', + 'Ruby', + 'Saxpath', + 'SCEA', + 'SCRIP', + 'Sendmail', + 'Sleepycat', + 'SLIB', + 'SNIA', + 'SISSL', + 'SPL', + 'TCL', + 'Teeworlds', + 'TPL', + 'Threeparttable', + 'TMate', + 'TORQUEv1.1', + 'TOSL', + 'UCD', + 'Vim', + 'VNLSL', + 'VOSTROM', + 'VSL', + 'W3C', + 'Webmin', + 'Wsuipa', + 'wxWidgets', + 'xinetd', + 'Xerox', + 'XSkat', + 'YPLv1.1', + 'Zed', + 'Zend', + 'ZPLv1.0', + 'ZPLv2.0', + 'ZPLv2.1', + 'zlib', + 'zlib with acknowledgement', + # Documentation + 'CDL', + 'CC-BY', + 'CC-BY-SA', + 'FBSDDL', + 'GFDL', + 'IEEE', + 'LDPL', + 'OFSFDL', + 'Open Publication', + 'Public Use', + # Content + 'CC-BY-ND', + 'DSL', + 'DMTF', + 'OAL', + 'EFML', + 'Free Art', + 'GeoGratis', + 'Green OpenMusic', + # Fonts + 'OFL', + 'Utopia', + 'AMS', + 'Arphic', + 'Baekmuk', + 'Bitstream Vera', + 'Charter', + 'DoubleStroke', + 'ec', + 'Elvish', + 'Hershey', + 'IPA', + 'Liberation', + 'Lucida', + 'MgOpen', + 'mplus', + 'PTFL', + 'STIX', + 'Wadalab', + 'XANO', + ]) diff --git a/sugar_network/zerosugar/spec.py b/sugar_network/zerosugar/spec.py new file mode 100644 index 0000000..4e79956 --- /dev/null +++ b/sugar_network/zerosugar/spec.py @@ -0,0 +1,474 @@ +# 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 <http://www.gnu.org/licenses/>. + +import re +import os +import logging +from os.path import join, exists, dirname +from ConfigParser import ConfigParser + +from sugar_network.zerosugar.licenses import GOOD_LICENSES +from active_toolkit import util, enforce + + +_LIST_SEPARATOR = ';' + +_POLICY_URL = 'http://wiki.sugarlabs.org/go/Sugar_Network/Policy' + +_FIELDS = { + # name: (required, typecast) + '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') + +_VERSION_RE = re.compile('-([a-z]*)') +_VERSION_MOD_TO_VALUE = { + 'pre': -2, + 'rc': -1, + '': 0, + 'post': 1, + } +_VERSION_VALUE_TO_MOD = {} + +_RESTRICTION_RE = re.compile('(>=|<|=)\s*([0-9.]+)') + +_logger = logging.getLogger('sweets-recipe') + + +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._config.get(section, key) + + def __repr__(self): + return '<Spec %s>' % self['Activity', 'bundle_id'] + + 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: + # 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 in self['icon']: + self._fields['icon'] = join('activity', self['icon'] + '.svg') + if not self['license']: + self._fields['license'] = 'license is not specified' + + 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(parse_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)) + + +def parse_version(version_string): + """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) + if parts[-1] == '': + del parts[-1] # Ends with a modifier + else: + parts.append('') + enforce(parts, ValueError, 'Empty version string') + + length = len(parts) + try: + for x in range(0, length, 2): + part = parts[x] + if part: + parts[x] = [int(i) for i in parts[x].split('.')] + else: + parts[x] = [] # (because ''.split('.') == [''], not []) + for x in range(1, length, 2): + parts[x] = _VERSION_MOD_TO_VALUE[parts[x]] + return parts + except ValueError as error: + util.exception() + raise RuntimeError('Invalid version format in "%s": %s' % + (version_string, error)) + except KeyError as error: + raise RuntimeError('Invalid version modifier in "%s": %s' % + (version_string, error)) + + +def format_version(version): + """Convert internal version representation back to string.""" + if version is None: + return None + + 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) + + +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 = {} + + +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_requires(requires): + result = {} + + for dep_str in _parse_list(requires): + dep = {} + + 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] == '>=': + not_before = format_version(parse_version(parts[1])) + elif parts[0] == '<': + before = format_version(parse_version(parts[1])) + elif parts[0] == '=': + not_before = format_version(parse_version(parts[1])) + before = parse_version(parts[1]) + before[-2][-1] += 1 + before = format_version(before) + del parts[:3] + + enforce(not parts or not parts[0].strip(), + 'Cannot parse "%s", it should be in format ' + '"<dependency> (>=|<|=) <version>"', dep_str) + + if before or not_before: + dep.setdefault('restrictions', []) + dep['restrictions'].append((not_before, before)) + + return 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] == _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()] diff --git a/sweets.recipe b/sweets.recipe index 8beae38..aed23e6 100644 --- a/sweets.recipe +++ b/sweets.recipe @@ -12,8 +12,8 @@ version = 0.5 stability = developer requires = m2crypto; requests; rrdtool-python; openssh-client; pylru - active-document; sweets-recipe; sugar-network-webui -replaces = sugar-network-server + active-document; sugar-network-webui +replaces = sugar-network-server; sweets-recipe [Build] install = install -m 0755 -d %(DESTDIR)s/%(PYTHONSITEDIR)s && diff --git a/tests/units/__main__.py b/tests/units/__main__.py index 0eac03b..5ecbaa6 100644 --- a/tests/units/__main__.py +++ b/tests/units/__main__.py @@ -3,6 +3,7 @@ from __init__ import tests from collection import * +from spec import * from volume import * from local import * from node import * diff --git a/tests/units/spec.py b/tests/units/spec.py new file mode 100755 index 0000000..1a3745b --- /dev/null +++ b/tests/units/spec.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# sugar-lint: disable + +from cStringIO import StringIO + +from __init__ import tests + +from sugar_network.zerosugar import spec + + +class SpecTest(tests.Test): + + def test_parse_requires(self): + self.assertEqual( + {'a': {}, 'b': {}, 'c': {}}, + spec._parse_requires('a; b; c')) + + self.assertEqual( + { + 'a': {'restrictions': [('1', '2')]}, + 'b': {'restrictions': [('1.2', '1.3')]}, + 'c': {'restrictions': [('2.2', None)]}, + 'd': {'restrictions': [(None, '3')]}, + }, + spec._parse_requires('a = 1; b=1.2; c>= 2.2; d <3-3')) + + self.assertEqual( + { + 'a': {'importance': 'recommended'}, + 'b': {}, + 'c': {'importance': 'recommended', 'restrictions': [(None, '1')]}, + }, + spec._parse_requires('[a]; b; [c<1]')) + + def test_parse_bindings(self): + self.assertEqual( + [ + ('bind1', '', 'prepend'), + ('bind2', '', 'prepend'), + ('bind3', '', 'prepend'), + ], + spec._parse_bindings(' bind1; prepend bind2;bind3 ')) + + self.assertEqual( + [ + ('bind1', '', 'append'), + ('bind2', 'foo', 'append'), + ], + spec._parse_bindings('append bind1; append bind2 foo')) + + self.assertEqual( + [ + ('bind1', '', 'replace'), + ('bind2', 'foo', 'replace'), + ], + spec._parse_bindings('replace bind1; replace bind2 foo')) + + def test_ActivityInfo(self): + stream = StringIO() + stream.write('\n'.join([ + '[Activity]', + 'name = Terminal', + 'activity_version = 35', + 'bundle_id = org.laptop.Terminal', + 'exec = sugar-activity terminal.TerminalActivity', + 'icon = activity-terminal', + 'mime_types = image/png;image/svg+xml', + 'license = GPLv2+', + 'tags = terminal; console', + 'requires = sugar = 0.94', + ])) + stream.seek(0) + + recipe = spec.Spec(stream) + self.assertEqual('Terminal', recipe['name']) + self.assertEqual('Terminal', recipe['summary']) + self.assertEqual('Terminal', recipe['description']) + self.assertEqual(['GPLv2+'], recipe['license']) + self.assertEqual('http://wiki.sugarlabs.org/go/Activities/Terminal', recipe['homepage']) + self.assertEqual('activity/activity-terminal.svg', recipe['icon']) + self.assertEqual('35', recipe['version']) + self.assertEqual('stable', recipe['stability']) + self.assertEqual(['terminal', 'console'], recipe['tags']) + self.assertEqual(['image/png', 'image/svg+xml'], recipe['mime_types']) + self.assertEqual( + { + 'activity': { + 'exec': 'sugar-activity terminal.TerminalActivity', + }, + }, + recipe.commands) + self.assertEqual( + { + 'sugar': { + 'restrictions': [('0.94', '0.95')], + }, + }, + recipe.requires) + + +if __name__ == '__main__': + tests.main() |