Rename the module to sugar3
The old gtk-2 based module will be present in the 0.94 branch in the sugar-toolkit. Signed-off-by: Simon Schampijer <simon@laptop.org> Acked-by: Sascha Silbe <silbe@activitycentral.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
sugardir = $(pythondir)/sugar3/bundle
|
||||
sugar_PYTHON = \
|
||||
__init__.py \
|
||||
bundle.py \
|
||||
activitybundle.py \
|
||||
bundleversion.py \
|
||||
contentbundle.py
|
||||
@@ -0,0 +1,16 @@
|
||||
# Copyright (C) 2006-2007, Red Hat, Inc.
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
# Boston, MA 02111-1307, USA.
|
||||
@@ -0,0 +1,341 @@
|
||||
# Copyright (C) 2007, Red Hat, Inc.
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
# Boston, MA 02111-1307, USA.
|
||||
|
||||
"""Sugar activity bundles
|
||||
|
||||
UNSTABLE.
|
||||
"""
|
||||
|
||||
from ConfigParser import ConfigParser
|
||||
import locale
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
from sugar import env
|
||||
from sugar import util
|
||||
from sugar.bundle.bundle import Bundle, \
|
||||
MalformedBundleException, NotInstalledException
|
||||
from sugar.bundle.bundleversion import NormalizedVersion
|
||||
from sugar.bundle.bundleversion import InvalidVersionError
|
||||
|
||||
|
||||
class ActivityBundle(Bundle):
|
||||
"""A Sugar activity bundle
|
||||
|
||||
See http://wiki.laptop.org/go/Activity_bundles for details
|
||||
"""
|
||||
|
||||
MIME_TYPE = 'application/vnd.olpc-sugar'
|
||||
DEPRECATED_MIME_TYPE = 'application/vnd.olpc-x-sugar'
|
||||
|
||||
_zipped_extension = '.xo'
|
||||
_unzipped_extension = '.activity'
|
||||
_infodir = 'activity'
|
||||
|
||||
def __init__(self, path):
|
||||
Bundle.__init__(self, path)
|
||||
self.activity_class = None
|
||||
self.bundle_exec = None
|
||||
|
||||
self._name = None
|
||||
self._local_name = None
|
||||
self._icon = None
|
||||
self._bundle_id = None
|
||||
self._mime_types = None
|
||||
self._show_launcher = True
|
||||
self._tags = None
|
||||
self._activity_version = '0'
|
||||
self._installation_time = os.stat(path).st_mtime
|
||||
|
||||
info_file = self.get_file('activity/activity.info')
|
||||
if info_file is None:
|
||||
raise MalformedBundleException('No activity.info file')
|
||||
self._parse_info(info_file)
|
||||
|
||||
linfo_file = self._get_linfo_file()
|
||||
if linfo_file:
|
||||
self._parse_linfo(linfo_file)
|
||||
|
||||
if self._local_name == None:
|
||||
self._local_name = self._name
|
||||
|
||||
def _parse_info(self, info_file):
|
||||
cp = ConfigParser()
|
||||
cp.readfp(info_file)
|
||||
|
||||
section = 'Activity'
|
||||
|
||||
if cp.has_option(section, 'bundle_id'):
|
||||
self._bundle_id = cp.get(section, 'bundle_id')
|
||||
# FIXME deprecated
|
||||
elif cp.has_option(section, 'service_name'):
|
||||
warnings.warn('use bundle_id instead of service_name ' \
|
||||
'in your activity.info', DeprecationWarning)
|
||||
self._bundle_id = cp.get(section, 'service_name')
|
||||
else:
|
||||
raise MalformedBundleException(
|
||||
'Activity bundle %s does not specify a bundle id' %
|
||||
self._path)
|
||||
|
||||
if cp.has_option(section, 'name'):
|
||||
self._name = cp.get(section, 'name')
|
||||
else:
|
||||
raise MalformedBundleException(
|
||||
'Activity bundle %s does not specify a name' % self._path)
|
||||
|
||||
# FIXME class is deprecated
|
||||
if cp.has_option(section, 'class'):
|
||||
warnings.warn('use exec instead of class ' \
|
||||
'in your activity.info', DeprecationWarning)
|
||||
self.activity_class = cp.get(section, 'class')
|
||||
elif cp.has_option(section, 'exec'):
|
||||
self.bundle_exec = cp.get(section, 'exec')
|
||||
else:
|
||||
raise MalformedBundleException(
|
||||
'Activity bundle %s must specify either class or exec' %
|
||||
self._path)
|
||||
|
||||
if cp.has_option(section, 'mime_types'):
|
||||
mime_list = cp.get(section, 'mime_types').strip(';')
|
||||
self._mime_types = [mime.strip() for mime in mime_list.split(';')]
|
||||
|
||||
if cp.has_option(section, 'show_launcher'):
|
||||
if cp.get(section, 'show_launcher') == 'no':
|
||||
self._show_launcher = False
|
||||
|
||||
if cp.has_option(section, 'tags'):
|
||||
tag_list = cp.get(section, 'tags').strip(';')
|
||||
self._tags = [tag.strip() for tag in tag_list.split(';')]
|
||||
|
||||
if cp.has_option(section, 'icon'):
|
||||
self._icon = cp.get(section, 'icon')
|
||||
|
||||
if cp.has_option(section, 'activity_version'):
|
||||
version = cp.get(section, 'activity_version')
|
||||
try:
|
||||
NormalizedVersion(version)
|
||||
except InvalidVersionError:
|
||||
raise MalformedBundleException(
|
||||
'Activity bundle %s has invalid version number %s' %
|
||||
(self._path, version))
|
||||
self._activity_version = version
|
||||
|
||||
def _get_linfo_file(self):
|
||||
lang = locale.getdefaultlocale()[0]
|
||||
if not lang:
|
||||
return None
|
||||
|
||||
linfo_path = os.path.join('locale', lang, 'activity.linfo')
|
||||
linfo_file = self.get_file(linfo_path)
|
||||
if linfo_file is not None:
|
||||
return linfo_file
|
||||
|
||||
linfo_path = os.path.join('locale', lang[:2], 'activity.linfo')
|
||||
linfo_file = self.get_file(linfo_path)
|
||||
if linfo_file is not None:
|
||||
return linfo_file
|
||||
|
||||
return None
|
||||
|
||||
def _parse_linfo(self, linfo_file):
|
||||
cp = ConfigParser()
|
||||
cp.readfp(linfo_file)
|
||||
|
||||
section = 'Activity'
|
||||
|
||||
if cp.has_option(section, 'name'):
|
||||
self._local_name = cp.get(section, 'name')
|
||||
|
||||
if cp.has_option(section, 'tags'):
|
||||
tag_list = cp.get(section, 'tags').strip(';')
|
||||
self._tags = [tag.strip() for tag in tag_list.split(';')]
|
||||
|
||||
def get_locale_path(self):
|
||||
"""Get the locale path inside the (installed) activity bundle."""
|
||||
if self._zip_file is not None:
|
||||
raise NotInstalledException
|
||||
return os.path.join(self._path, 'locale')
|
||||
|
||||
def get_icons_path(self):
|
||||
"""Get the icons path inside the (installed) activity bundle."""
|
||||
if self._zip_file is not None:
|
||||
raise NotInstalledException
|
||||
return os.path.join(self._path, 'icons')
|
||||
|
||||
def get_path(self):
|
||||
"""Get the activity bundle path."""
|
||||
return self._path
|
||||
|
||||
def get_name(self):
|
||||
"""Get the activity user-visible name."""
|
||||
return self._local_name
|
||||
|
||||
def get_bundle_name(self):
|
||||
"""Get the activity bundle name."""
|
||||
return self._name
|
||||
|
||||
def get_installation_time(self):
|
||||
"""Get a timestamp representing the time at which this activity was
|
||||
installed."""
|
||||
return self._installation_time
|
||||
|
||||
def get_bundle_id(self):
|
||||
"""Get the activity bundle id"""
|
||||
return self._bundle_id
|
||||
|
||||
def get_icon(self):
|
||||
"""Get the activity icon name"""
|
||||
# FIXME: this should return the icon data, not a filename, so that
|
||||
# we don't need to create a temp file in the zip case
|
||||
icon_path = os.path.join('activity', self._icon + '.svg')
|
||||
if self._zip_file is None:
|
||||
return os.path.join(self._path, icon_path)
|
||||
else:
|
||||
icon_data = self.get_file(icon_path).read()
|
||||
temp_file, temp_file_path = tempfile.mkstemp(prefix=self._icon,
|
||||
suffix='.svg')
|
||||
os.write(temp_file, icon_data)
|
||||
os.close(temp_file)
|
||||
return util.TempFilePath(temp_file_path)
|
||||
|
||||
def get_activity_version(self):
|
||||
"""Get the activity version"""
|
||||
return self._activity_version
|
||||
|
||||
def get_command(self):
|
||||
"""Get the command to execute to launch the activity factory"""
|
||||
if self.bundle_exec:
|
||||
command = os.path.expandvars(self.bundle_exec)
|
||||
else:
|
||||
command = 'sugar-activity ' + self.activity_class
|
||||
|
||||
return command
|
||||
|
||||
def get_mime_types(self):
|
||||
"""Get the MIME types supported by the activity"""
|
||||
return self._mime_types
|
||||
|
||||
def get_tags(self):
|
||||
"""Get the tags that describe the activity"""
|
||||
return self._tags
|
||||
|
||||
def get_show_launcher(self):
|
||||
"""Get whether there should be a visible launcher for the activity"""
|
||||
return self._show_launcher
|
||||
|
||||
def install(self, install_dir=None):
|
||||
if install_dir is None:
|
||||
install_dir = env.get_user_activities_path()
|
||||
|
||||
self._unzip(install_dir)
|
||||
|
||||
install_path = os.path.join(install_dir, self._zip_root_dir)
|
||||
self.install_mime_type(install_path)
|
||||
|
||||
return install_path
|
||||
|
||||
def install_mime_type(self, install_path):
|
||||
""" Update the mime type database and install the mime type icon
|
||||
"""
|
||||
xdg_data_home = os.getenv('XDG_DATA_HOME',
|
||||
os.path.expanduser('~/.local/share'))
|
||||
|
||||
mime_path = os.path.join(install_path, 'activity', 'mimetypes.xml')
|
||||
if os.path.isfile(mime_path):
|
||||
mime_dir = os.path.join(xdg_data_home, 'mime')
|
||||
mime_pkg_dir = os.path.join(mime_dir, 'packages')
|
||||
if not os.path.isdir(mime_pkg_dir):
|
||||
os.makedirs(mime_pkg_dir)
|
||||
installed_mime_path = os.path.join(mime_pkg_dir,
|
||||
'%s.xml' % self._bundle_id)
|
||||
self._symlink(mime_path, installed_mime_path)
|
||||
os.spawnlp(os.P_WAIT, 'update-mime-database',
|
||||
'update-mime-database', mime_dir)
|
||||
|
||||
mime_types = self.get_mime_types()
|
||||
if mime_types is not None:
|
||||
installed_icons_dir = os.path.join(xdg_data_home,
|
||||
'icons/sugar/scalable/mimetypes')
|
||||
if not os.path.isdir(installed_icons_dir):
|
||||
os.makedirs(installed_icons_dir)
|
||||
|
||||
for mime_type in mime_types:
|
||||
mime_icon_base = os.path.join(install_path, 'activity',
|
||||
mime_type.replace('/', '-'))
|
||||
svg_file = mime_icon_base + '.svg'
|
||||
info_file = mime_icon_base + '.icon'
|
||||
self._symlink(svg_file,
|
||||
os.path.join(installed_icons_dir,
|
||||
os.path.basename(svg_file)))
|
||||
self._symlink(info_file,
|
||||
os.path.join(installed_icons_dir,
|
||||
os.path.basename(info_file)))
|
||||
|
||||
def _symlink(self, src, dst):
|
||||
if not os.path.isfile(src):
|
||||
return
|
||||
if not os.path.islink(dst) and os.path.exists(dst):
|
||||
raise RuntimeError('Do not remove %s if it was not '
|
||||
'installed by sugar', dst)
|
||||
logging.debug('Link resource %s to %s', src, dst)
|
||||
if os.path.lexists(dst):
|
||||
logging.debug('Relink %s', dst)
|
||||
os.unlink(dst)
|
||||
os.symlink(src, dst)
|
||||
|
||||
def uninstall(self, install_path, force=False, delete_profile=False):
|
||||
if os.path.islink(install_path):
|
||||
# Don't remove the actual activity dir if it's a symbolic link
|
||||
# because we may be removing user data.
|
||||
os.unlink(install_path)
|
||||
return
|
||||
|
||||
xdg_data_home = os.getenv('XDG_DATA_HOME',
|
||||
os.path.expanduser('~/.local/share'))
|
||||
|
||||
mime_dir = os.path.join(xdg_data_home, 'mime')
|
||||
installed_mime_path = os.path.join(mime_dir, 'packages',
|
||||
'%s.xml' % self._bundle_id)
|
||||
if os.path.exists(installed_mime_path):
|
||||
os.remove(installed_mime_path)
|
||||
os.spawnlp(os.P_WAIT, 'update-mime-database',
|
||||
'update-mime-database', mime_dir)
|
||||
|
||||
mime_types = self.get_mime_types()
|
||||
if mime_types is not None:
|
||||
installed_icons_dir = os.path.join(xdg_data_home,
|
||||
'icons/sugar/scalable/mimetypes')
|
||||
if os.path.isdir(installed_icons_dir):
|
||||
for f in os.listdir(installed_icons_dir):
|
||||
path = os.path.join(installed_icons_dir, f)
|
||||
if os.path.islink(path) and \
|
||||
os.readlink(path).startswith(install_path):
|
||||
os.remove(path)
|
||||
|
||||
if delete_profile:
|
||||
bundle_profile_path = env.get_profile_path(self._bundle_id)
|
||||
if os.path.exists(bundle_profile_path):
|
||||
os.chmod(bundle_profile_path, 0775)
|
||||
shutil.rmtree(bundle_profile_path, ignore_errors=True)
|
||||
|
||||
self._uninstall(install_path)
|
||||
|
||||
def is_user_activity(self):
|
||||
return self.get_path().startswith(env.get_user_activities_path())
|
||||
@@ -0,0 +1,200 @@
|
||||
# Copyright (C) 2007, Red Hat, Inc.
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
# Boston, MA 02111-1307, USA.
|
||||
|
||||
"""Sugar bundle file handler
|
||||
|
||||
UNSTABLE.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import shutil
|
||||
import StringIO
|
||||
import zipfile
|
||||
|
||||
|
||||
class AlreadyInstalledException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NotInstalledException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidPathException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ZipExtractException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RegistrationException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MalformedBundleException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Bundle(object):
|
||||
"""A Sugar activity, content module, etc.
|
||||
|
||||
The bundle itself may be either a zip file or a directory
|
||||
hierarchy, with metadata about the bundle stored various files
|
||||
inside it.
|
||||
|
||||
This is an abstract base class. See ActivityBundle and
|
||||
ContentBundle for more details on those bundle types.
|
||||
"""
|
||||
|
||||
_zipped_extension = None
|
||||
_unzipped_extension = None
|
||||
|
||||
def __init__(self, path):
|
||||
self._path = path
|
||||
self._zip_root_dir = None
|
||||
self._zip_file = None
|
||||
|
||||
if not os.path.isdir(self._path):
|
||||
try:
|
||||
self._zip_file = zipfile.ZipFile(self._path)
|
||||
except zipfile.error, exception:
|
||||
raise MalformedBundleException('Error accessing zip file %r: '
|
||||
'%s' % (self._path, exception))
|
||||
self._check_zip_bundle()
|
||||
|
||||
def __del__(self):
|
||||
if self._zip_file is not None:
|
||||
self._zip_file.close()
|
||||
|
||||
def _check_zip_bundle(self):
|
||||
file_names = self._zip_file.namelist()
|
||||
if len(file_names) == 0:
|
||||
raise MalformedBundleException('Empty zip file')
|
||||
|
||||
if file_names[0] == 'mimetype':
|
||||
del file_names[0]
|
||||
|
||||
self._zip_root_dir = file_names[0].split('/')[0]
|
||||
if self._zip_root_dir.startswith('.'):
|
||||
raise MalformedBundleException(
|
||||
'root directory starts with .')
|
||||
if self._unzipped_extension is not None:
|
||||
(name_, ext) = os.path.splitext(self._zip_root_dir)
|
||||
if ext != self._unzipped_extension:
|
||||
raise MalformedBundleException(
|
||||
'All files in the bundle must be inside a single ' +
|
||||
'directory whose name ends with %r' %
|
||||
self._unzipped_extension)
|
||||
|
||||
for file_name in file_names:
|
||||
if not file_name.startswith(self._zip_root_dir):
|
||||
raise MalformedBundleException(
|
||||
'All files in the bundle must be inside a single ' +
|
||||
'top-level directory')
|
||||
|
||||
def get_file(self, filename):
|
||||
f = None
|
||||
|
||||
if self._zip_file is None:
|
||||
path = os.path.join(self._path, filename)
|
||||
try:
|
||||
f = open(path, 'rb')
|
||||
except IOError:
|
||||
return None
|
||||
else:
|
||||
path = os.path.join(self._zip_root_dir, filename)
|
||||
try:
|
||||
data = self._zip_file.read(path)
|
||||
f = StringIO.StringIO(data)
|
||||
except KeyError:
|
||||
logging.debug('%s not found.', filename)
|
||||
|
||||
return f
|
||||
|
||||
def is_file(self, filename):
|
||||
if self._zip_file is None:
|
||||
path = os.path.join(self._path, filename)
|
||||
return os.path.isfile(path)
|
||||
else:
|
||||
path = os.path.join(self._zip_root_dir, filename)
|
||||
try:
|
||||
self._zip_file.getinfo(path)
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_dir(self, filename):
|
||||
if self._zip_file is None:
|
||||
path = os.path.join(self._path, filename)
|
||||
return os.path.isdir(path)
|
||||
else:
|
||||
path = os.path.join(self._zip_root_dir, filename, "")
|
||||
for f in self._zip_file.namelist():
|
||||
if f.startswith(path):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_path(self):
|
||||
"""Get the bundle path."""
|
||||
return self._path
|
||||
|
||||
def _unzip(self, install_dir):
|
||||
if self._zip_file is None:
|
||||
raise AlreadyInstalledException
|
||||
|
||||
if not os.path.isdir(install_dir):
|
||||
os.mkdir(install_dir, 0775)
|
||||
|
||||
# zipfile provides API that in theory would let us do this
|
||||
# correctly by hand, but handling all the oddities of
|
||||
# Windows/UNIX mappings, extension attributes, deprecated
|
||||
# features, etc makes it impractical.
|
||||
if os.spawnlp(os.P_WAIT, 'unzip', 'unzip', '-o', self._path,
|
||||
'-x', 'mimetype', '-d', install_dir):
|
||||
# clean up install dir after failure
|
||||
shutil.rmtree(os.path.join(install_dir, self._zip_root_dir),
|
||||
ignore_errors=True)
|
||||
# indicate failure.
|
||||
raise ZipExtractException
|
||||
|
||||
def _zip(self, bundle_path):
|
||||
if self._zip_file is not None:
|
||||
raise NotInstalledException
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def _uninstall(self, install_path):
|
||||
if not os.path.isdir(install_path):
|
||||
raise InvalidPathException
|
||||
if self._unzipped_extension is not None:
|
||||
(name_, ext) = os.path.splitext(install_path)
|
||||
if ext != self._unzipped_extension:
|
||||
raise InvalidPathException
|
||||
|
||||
for root, dirs, files in os.walk(install_path, topdown=False):
|
||||
for name in files:
|
||||
os.remove(os.path.join(root, name))
|
||||
for name in dirs:
|
||||
path = os.path.join(root, name)
|
||||
if os.path.islink(path):
|
||||
os.remove(path)
|
||||
else:
|
||||
os.rmdir(path)
|
||||
os.rmdir(install_path)
|
||||
@@ -0,0 +1,157 @@
|
||||
# Copyright (C) 2010, OLPC
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
# Boston, MA 02111-1307, USA.
|
||||
|
||||
#
|
||||
# Based on the implementation of PEP 386, but adapted to our
|
||||
# numeration schema.
|
||||
#
|
||||
|
||||
import re
|
||||
|
||||
|
||||
class InvalidVersionError(Exception):
|
||||
"""The passed activity version can not be normalized."""
|
||||
pass
|
||||
|
||||
VERSION_RE = re.compile(r'''
|
||||
^
|
||||
(?P<version>\d+) # minimum 'N'
|
||||
(?P<extraversion>(?:\.\d+)*) # any number of extra '.N' segments
|
||||
(?:
|
||||
(?P<local>\-[a-zA-Z]*) # ignore any string in the comparison
|
||||
)?
|
||||
$''', re.VERBOSE)
|
||||
|
||||
|
||||
class NormalizedVersion(object):
|
||||
"""A normalized version.
|
||||
|
||||
Good:
|
||||
1
|
||||
1.2
|
||||
1.2.3
|
||||
1.2.3-peru
|
||||
|
||||
Bad:
|
||||
1.2peru # must be separated with -
|
||||
1.2. # can't end with '.'
|
||||
1.02.5 # can't have a leading zero
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, activity_version):
|
||||
"""Create a NormalizedVersion instance from a version string.
|
||||
|
||||
Keyword arguments:
|
||||
activity_version -- The version string
|
||||
|
||||
"""
|
||||
self._activity_version = activity_version
|
||||
self.parts = []
|
||||
self._local = None
|
||||
|
||||
if not isinstance(self._activity_version, str):
|
||||
raise InvalidVersionError(self._activity_version)
|
||||
|
||||
match = VERSION_RE.search(self._activity_version)
|
||||
if not match:
|
||||
raise InvalidVersionError(self._activity_version)
|
||||
|
||||
groups = match.groupdict()
|
||||
|
||||
version = self._parse_version(groups['version'])
|
||||
self.parts.append(version)
|
||||
|
||||
if groups['extraversion'] not in ('', None):
|
||||
versions = self._parse_extraversions(groups['extraversion'][1:])
|
||||
self.parts.extend(versions)
|
||||
|
||||
self._local = groups['local']
|
||||
|
||||
def _parse_version(self, version_string):
|
||||
"""Verify that there is no leading zero and convert to integer.
|
||||
|
||||
Keyword arguments:
|
||||
version -- string to be parsed
|
||||
|
||||
Return: Version
|
||||
|
||||
"""
|
||||
if len(version_string) > 1 and version_string[0] == '0':
|
||||
raise InvalidVersionError("Can not have leading zero in segment"
|
||||
" %s in %r" % (version_string,
|
||||
self._activity_version))
|
||||
|
||||
return int(version_string)
|
||||
|
||||
def _parse_extraversions(self, extraversion_string):
|
||||
"""Split into N versions and convert them to integers, verify
|
||||
that there are no leading zeros and drop trailing zeros.
|
||||
|
||||
Keyword arguments:
|
||||
extraversion -- 'N.N.N...' sequence to be parsed
|
||||
|
||||
Return: List of extra versions
|
||||
|
||||
"""
|
||||
nums = []
|
||||
for n in extraversion_string.split("."):
|
||||
if len(n) > 1 and n[0] == '0':
|
||||
raise InvalidVersionError("Can not have leading zero in "
|
||||
"segment %s in %r" % (n,
|
||||
self._activity_version))
|
||||
nums.append(int(n))
|
||||
|
||||
while nums and nums[-1] == 0:
|
||||
nums.pop()
|
||||
|
||||
return nums
|
||||
|
||||
def __str__(self):
|
||||
version_string = '.'.join(str(v) for v in self.parts)
|
||||
if self._local != None:
|
||||
version_string += self._local
|
||||
return version_string
|
||||
|
||||
def __repr__(self):
|
||||
return "%s('%s')" % (self.__class__.__name__, self)
|
||||
|
||||
def _cannot_compare(self, other):
|
||||
raise TypeError("Can not compare %s and %s"
|
||||
% (type(self).__name__, type(other).__name__))
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, NormalizedVersion):
|
||||
self._cannot_compare(other)
|
||||
return self.parts == other.parts
|
||||
|
||||
def __lt__(self, other):
|
||||
if not isinstance(other, NormalizedVersion):
|
||||
self._cannot_compare(other)
|
||||
return self.parts < other.parts
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __gt__(self, other):
|
||||
return not (self.__lt__(other) or self.__eq__(other))
|
||||
|
||||
def __le__(self, other):
|
||||
return self.__eq__(other) or self.__lt__(other)
|
||||
|
||||
def __ge__(self, other):
|
||||
return self.__eq__(other) or self.__gt__(other)
|
||||
@@ -0,0 +1,243 @@
|
||||
# Copyright (C) 2007, Red Hat, Inc.
|
||||
# Copyright (C) 2009 Aleksey Lim
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
# Boston, MA 02111-1307, USA.
|
||||
|
||||
"""Sugar content bundles
|
||||
|
||||
UNSTABLE.
|
||||
"""
|
||||
|
||||
from ConfigParser import ConfigParser
|
||||
import os
|
||||
import urllib
|
||||
|
||||
from sugar import env
|
||||
from sugar.bundle.bundle import Bundle, NotInstalledException, \
|
||||
MalformedBundleException
|
||||
|
||||
from sugar.bundle.bundleversion import NormalizedVersion
|
||||
from sugar.bundle.bundleversion import InvalidVersionError
|
||||
|
||||
|
||||
class ContentBundle(Bundle):
|
||||
"""A Sugar content bundle
|
||||
|
||||
See http://wiki.laptop.org/go/Content_bundles for details
|
||||
"""
|
||||
|
||||
MIME_TYPE = 'application/vnd.olpc-content'
|
||||
|
||||
_zipped_extension = '.xol'
|
||||
_unzipped_extension = None
|
||||
_infodir = 'library'
|
||||
|
||||
def __init__(self, path):
|
||||
Bundle.__init__(self, path)
|
||||
|
||||
self._locale = None
|
||||
self._l10n = None
|
||||
self._category = None
|
||||
self._name = None
|
||||
self._subcategory = None
|
||||
self._category_class = None
|
||||
self._category_icon = None
|
||||
self._library_version = '0'
|
||||
self._bundle_class = None
|
||||
self._activity_start = None
|
||||
self._global_name = None
|
||||
|
||||
info_file = self.get_file('library/library.info')
|
||||
if info_file is None:
|
||||
raise MalformedBundleException('No library.info file')
|
||||
self._parse_info(info_file)
|
||||
|
||||
if (self.get_file('index.html') is None and
|
||||
self.get_file('library/library.xml') is None):
|
||||
raise MalformedBundleException(
|
||||
'Content bundle %s has neither index.html nor library.xml' %
|
||||
self._path)
|
||||
|
||||
def _parse_info(self, info_file):
|
||||
cp = ConfigParser()
|
||||
cp.readfp(info_file)
|
||||
|
||||
section = 'Library'
|
||||
|
||||
if cp.has_option(section, 'name'):
|
||||
self._name = cp.get(section, 'name')
|
||||
else:
|
||||
raise MalformedBundleException(
|
||||
'Content bundle %s does not specify a name' % self._path)
|
||||
|
||||
if cp.has_option(section, 'library_version'):
|
||||
version = cp.get(section, 'library_version')
|
||||
try:
|
||||
NormalizedVersion(version)
|
||||
except InvalidVersionError:
|
||||
raise MalformedBundleException(
|
||||
'Content bundle %s has invalid version number %s' %
|
||||
(self._path, version))
|
||||
self._library_version = version
|
||||
|
||||
if cp.has_option(section, 'l10n'):
|
||||
l10n = cp.get(section, 'l10n')
|
||||
if l10n == 'true':
|
||||
self._l10n = True
|
||||
elif l10n == 'false':
|
||||
self._l10n = False
|
||||
else:
|
||||
raise MalformedBundleException(
|
||||
'Content bundle %s has invalid l10n key %r' %
|
||||
(self._path, l10n))
|
||||
else:
|
||||
raise MalformedBundleException(
|
||||
'Content bundle %s does not specify if it is localized' %
|
||||
self._path)
|
||||
|
||||
if cp.has_option(section, 'locale'):
|
||||
self._locale = cp.get(section, 'locale')
|
||||
else:
|
||||
raise MalformedBundleException(
|
||||
'Content bundle %s does not specify a locale' % self._path)
|
||||
|
||||
if cp.has_option(section, 'category'):
|
||||
self._category = cp.get(section, 'category')
|
||||
else:
|
||||
raise MalformedBundleException(
|
||||
'Content bundle %s does not specify a category' % self._path)
|
||||
|
||||
if cp.has_option(section, 'global_name'):
|
||||
self._global_name = cp.get(section, 'global_name')
|
||||
else:
|
||||
self._global_name = None
|
||||
|
||||
if cp.has_option(section, 'category_icon'):
|
||||
self._category_icon = cp.get(section, 'category_icon')
|
||||
else:
|
||||
self._category_icon = None
|
||||
|
||||
if cp.has_option(section, 'category_class'):
|
||||
self._category_class = cp.get(section, 'category_class')
|
||||
else:
|
||||
self._category_class = None
|
||||
|
||||
if cp.has_option(section, 'subcategory'):
|
||||
self._subcategory = cp.get(section, 'subcategory')
|
||||
else:
|
||||
self._subcategory = None
|
||||
|
||||
if cp.has_option(section, 'bundle_class'):
|
||||
self._bundle_class = cp.get(section, 'bundle_class')
|
||||
else:
|
||||
self._bundle_class = None
|
||||
|
||||
if cp.has_option(section, 'activity_start'):
|
||||
self._activity_start = cp.get(section, 'activity_start')
|
||||
else:
|
||||
self._activity_start = 'index.html'
|
||||
|
||||
if self._bundle_class is None and self._global_name is None:
|
||||
raise MalformedBundleException(
|
||||
'Content bundle %s must specify either global_name or '
|
||||
'bundle_class' % self._path)
|
||||
|
||||
def get_name(self):
|
||||
return self._name
|
||||
|
||||
def get_library_version(self):
|
||||
return self._library_version
|
||||
|
||||
def get_l10n(self):
|
||||
return self._l10n
|
||||
|
||||
def get_locale(self):
|
||||
return self._locale
|
||||
|
||||
def get_category(self):
|
||||
return self._category
|
||||
|
||||
def get_category_icon(self):
|
||||
return self._category_icon
|
||||
|
||||
def get_category_class(self):
|
||||
return self._category_class
|
||||
|
||||
def get_subcategory(self):
|
||||
return self._subcategory
|
||||
|
||||
def get_bundle_class(self):
|
||||
return self._bundle_class
|
||||
|
||||
def get_activity_start(self):
|
||||
return self._activity_start
|
||||
|
||||
def _run_indexer(self):
|
||||
xdg_data_dirs = os.getenv('XDG_DATA_DIRS',
|
||||
'/usr/local/share/:/usr/share/')
|
||||
for path in xdg_data_dirs.split(':'):
|
||||
indexer = os.path.join(path, 'library-common', 'make_index.py')
|
||||
if os.path.exists(indexer):
|
||||
os.spawnlp(os.P_WAIT, 'python', 'python', indexer)
|
||||
|
||||
def get_root_dir(self):
|
||||
return os.path.join(env.get_user_library_path(), self._zip_root_dir)
|
||||
|
||||
def get_start_path(self):
|
||||
return os.path.join(self.get_root_dir(), self._activity_start)
|
||||
|
||||
def get_start_uri(self):
|
||||
return 'file://' + urllib.pathname2url(self.get_start_path())
|
||||
|
||||
def get_bundle_id(self):
|
||||
# TODO treat ContentBundle in special way
|
||||
# needs rethinking while fixing ContentBundle support
|
||||
if self._bundle_class is not None:
|
||||
return self._bundle_class
|
||||
else:
|
||||
return self._global_name
|
||||
|
||||
def get_activity_version(self):
|
||||
# TODO treat ContentBundle in special way
|
||||
# needs rethinking while fixing ContentBundle support
|
||||
return self._library_version
|
||||
|
||||
def is_installed(self):
|
||||
if self._zip_file is None:
|
||||
return True
|
||||
elif os.path.isdir(self.get_root_dir()):
|
||||
return ContentBundle(self.get_root_dir()).get_library_version() \
|
||||
== self.get_library_version()
|
||||
else:
|
||||
return False
|
||||
|
||||
def install(self):
|
||||
# TODO ignore passed install_path argument
|
||||
# needs rethinking while fixing ContentBundle support
|
||||
install_path = env.get_user_library_path()
|
||||
self._unzip(install_path)
|
||||
self._run_indexer()
|
||||
return self.get_root_dir()
|
||||
|
||||
def uninstall(self):
|
||||
if self._zip_file is None:
|
||||
if not self.is_installed():
|
||||
raise NotInstalledException
|
||||
install_dir = self._path
|
||||
else:
|
||||
install_dir = os.path.join(self.get_root_dir())
|
||||
self._uninstall(install_dir)
|
||||
self._run_indexer()
|
||||
Reference in New Issue
Block a user