Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksey Lim <alsroot@sugarlabs.org>2012-09-22 07:11:44 (GMT)
committer Aleksey Lim <alsroot@sugarlabs.org>2012-09-22 07:11:44 (GMT)
commitdfc1575a588c061a896c543452b9b75c4bbe1389 (patch)
treef139a8c92669e1ab3b9b2cc8a54c5c3377b8452d
parent046be81cb435826abbecff2cfb08b217ee742179 (diff)
Merge sweets-recipe project
-rwxr-xr-xmisc/aslo_sync.py2
-rw-r--r--sugar_network/local/activities.py2
-rw-r--r--sugar_network/local/mounts.py5
-rw-r--r--sugar_network/resources/implementation.py2
-rw-r--r--sugar_network/toolkit/http.py2
-rw-r--r--sugar_network/zerosugar/__init__.py4
-rw-r--r--sugar_network/zerosugar/bundle.py124
-rw-r--r--sugar_network/zerosugar/feeds.py11
-rw-r--r--sugar_network/zerosugar/injector.py3
-rw-r--r--sugar_network/zerosugar/licenses.py255
-rw-r--r--sugar_network/zerosugar/spec.py474
-rw-r--r--sweets.recipe4
-rw-r--r--tests/units/__main__.py1
-rwxr-xr-xtests/units/spec.py102
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()