From f377d6e5197f07b82c3ccd29e2899e02f03b3d8e Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Thu, 20 Sep 2007 16:20:21 +0000 Subject: Redo activity bundles in terms of sugar.bundle.bundle --- (limited to 'sugar') diff --git a/sugar/activity/Makefile.am b/sugar/activity/Makefile.am index d6ea9a4..d523a8d 100644 --- a/sugar/activity/Makefile.am +++ b/sugar/activity/Makefile.am @@ -6,6 +6,5 @@ sugar_PYTHON = \ activityfactoryservice.py \ activityhandle.py \ activityservice.py \ - bundle.py \ bundlebuilder.py \ registry.py diff --git a/sugar/activity/activityfactoryservice.py b/sugar/activity/activityfactoryservice.py index bd89b91..2e4ab6b 100644 --- a/sugar/activity/activityfactoryservice.py +++ b/sugar/activity/activityfactoryservice.py @@ -28,7 +28,7 @@ import dbus import dbus.service import dbus.glib -from sugar.activity.bundle import Bundle +from sugar.bundle.activitybundle import ActivityBundle from sugar.activity import activityhandle from sugar import logger from sugar import _sugarext @@ -155,7 +155,7 @@ def run_with_args(args): def run(bundle_path): sys.path.append(bundle_path) - bundle = Bundle(bundle_path) + bundle = ActivityBundle(bundle_path) logger.start(bundle.get_service_name()) diff --git a/sugar/activity/bundle.py b/sugar/activity/bundle.py deleted file mode 100644 index a5231ef..0000000 --- a/sugar/activity/bundle.py +++ /dev/null @@ -1,318 +0,0 @@ -# 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. - -"""Metadata description of a given application/activity""" - -import logging -import locale -import os -import zipfile -from ConfigParser import ConfigParser -import StringIO -import tempfile - -import dbus - -from sugar import env -from sugar import activity -from sugar.bundle.bundle import AlreadyInstalledException, \ - NotInstalledException, InvalidPathException, ZipExtractException, \ - RegistrationException, MalformedBundleException - -_PYTHON_FACTORY='sugar-activity-factory' - -_DBUS_SHELL_SERVICE = "org.laptop.Shell" -_DBUS_SHELL_PATH = "/org/laptop/Shell" -_DBUS_ACTIVITY_REGISTRY_IFACE = "org.laptop.Shell.ActivityRegistry" - -class Bundle: - """Metadata description of a given application/activity - - The metadata is normally read from an activity.info file, - which is an INI-style configuration file read using the - standard Python ConfigParser module. - - The format reference for the Bundle definition files is - available for further reference: - - http://wiki.laptop.org/go/Activity_bundles - """ - def __init__(self, path): - self._init_with_path(path) - - def _init_with_path(self, path): - self.activity_class = None - self.bundle_exec = None - - self._name = None - self._icon = None - self._service_name = None - self._mime_types = None - self._show_launcher = True - self._valid = True - self._path = path - self._activity_version = 0 - - info_file = self._get_info_file() - if info_file: - self._parse_info(info_file) - else: - self._valid = False - - linfo_file = self._get_linfo_file() - if linfo_file: - self._parse_linfo(linfo_file) - - def _get_info_file(self): - info_file = None - - if os.path.isdir(self._path): - info_path = os.path.join(self._path, 'activity', 'activity.info') - if os.path.isfile(info_path): - info_file = open(info_path) - else: - zip_file = zipfile.ZipFile(self._path) - file_names = zip_file.namelist() - root_dir = self._get_bundle_root_dir(file_names) - info_path = os.path.join(root_dir, 'activity', 'activity.info') - if info_path in file_names: - info_data = zip_file.read(info_path) - info_file = StringIO.StringIO(info_data) - zip_file.close() - - return info_file - - def _parse_info(self, info_file): - cp = ConfigParser() - cp.readfp(info_file) - - section = 'Activity' - - if cp.has_option(section, 'service_name'): - self._service_name = cp.get(section, 'service_name') - else: - self._valid = False - logging.error('%s must specify a service name' % self._path) - - if cp.has_option(section, 'name'): - self._name = cp.get(section, 'name') - else: - self._valid = False - logging.error('%s must specify a name' % self._path) - - if cp.has_option(section, 'class'): - self.activity_class = cp.get(section, 'class') - elif cp.has_option(section, 'exec'): - self.bundle_exec = cp.get(section, 'exec') - else: - self._valid = False - logging.error('%s must specify exec or class' % self._path) - - if cp.has_option(section, 'mime_types'): - mime_list = cp.get(section, 'mime_types') - self._mime_types = mime_list.strip(';').split(';') - - if cp.has_option(section, 'show_launcher'): - if cp.get(section, 'show_launcher') == 'no': - self._show_launcher = False - - 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: - self._activity_version = int(version) - except ValueError: - self._valid = False - - def _parse_linfo(self, linfo_file): - cp = ConfigParser() - cp.readfp(linfo_file) - - section = 'Activity' - - if cp.has_option(section, 'name'): - self._name = cp.get(section, 'name') - - def _get_linfo_file(self): - linfo_file = None - - lang = locale.getdefaultlocale()[0] - if not lang: - return None - - if os.path.isdir(self._path): - linfo_path = os.path.join(self.get_locale_path(), lang, 'activity.linfo') - if not os.path.isfile(linfo_path): - linfo_path = os.path.join(self.get_locale_path(), lang[:2], 'activity.linfo') - if os.path.isfile(linfo_path): - linfo_file = open(linfo_path) - else: - zip_file = zipfile.ZipFile(self._path) - file_names = zip_file.namelist() - root_dir = self._get_bundle_root_dir(file_names) - linfo_path = os.path.join(root_dir, 'locale', lang, 'activity.linfo') - if not linfo_path in file_names: - linfo_path = os.path.join(root_dir, 'locale', lang[:2], 'activity.linfo') - if linfo_path in zip_file.namelist(): - linfo_data = zip_file.read(linfo_path) - linfo_file = StringIO.StringIO(linfo_data) - - zip_file.close() - - return linfo_file - - def is_valid(self): - return self._valid - - def get_locale_path(self): - """Get the locale path inside the activity bundle.""" - return os.path.join(self._path, 'locale') - - def get_icons_path(self): - """Get the icons path inside the activity bundle.""" - 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._name - - def get_service_name(self): - """Get the activity service name""" - return self._service_name - - def get_icon(self): - """Get the activity icon name""" - if os.path.isdir(self._path): - activity_path = os.path.join(self._path, 'activity') - return os.path.join(activity_path, self._icon + '.svg') - else: - zip_file = zipfile.ZipFile(self._path) - file_names = zip_file.namelist() - root_dir = self._get_bundle_root_dir(file_names) - icon_path = os.path.join(root_dir, 'activity', self._icon + '.svg') - if icon_path in file_names: - icon_data = zip_file.read(icon_path) - temp_file, temp_file_path = tempfile.mkstemp(suffix='.svg', prefix=self._icon) - os.write(temp_file, icon_data) - os.close(temp_file) - return temp_file_path - else: - return None - - 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.join(self._path, self.bundle_exec) - command = command.replace('$SUGAR_BUNDLE_PATH', self._path) - command = os.path.expandvars(command) - else: - command = '%s --bundle-path="%s"' % ( - env.get_bin_path(_PYTHON_FACTORY), self._path) - - return command - - def get_class(self): - """Get the main Activity class""" - return self._class - - def get_mime_types(self): - """Get the MIME types supported by the activity""" - return self._mime_types - - def get_show_launcher(self): - """Get whether there should be a visible launcher for the activity""" - return self._show_launcher - - def is_installed(self): - if self._valid and activity.get_registry().get_activity(self._service_name): - return True - else: - return False - - def _get_bundle_root_dir(self, file_names): - """ - We check here that all the files in the .xo are inside one only dir - (bundle_root_dir). - """ - bundle_root_dir = None - for file_name in file_names: - if not bundle_root_dir: - bundle_root_dir = file_name.split('/')[0] - if not bundle_root_dir.endswith('.activity'): - raise MalformedBundleException( - 'The activity directory name must end with .activity') - else: - if not file_name.startswith(bundle_root_dir): - raise MalformedBundleException( - 'All files in the bundle must be inside the activity directory') - - return bundle_root_dir - - def install(self): - if self.is_installed(): - raise AlreadyInstalledException - - ext = os.path.splitext(self._path)[1] - if not os.path.isfile(self._path): - raise InvalidPathException - - bundle_dir = env.get_user_activities_path() - if not os.path.isdir(bundle_dir): - os.mkdir(bundle_dir) - - zip_file = zipfile.ZipFile(self._path) - file_names = zip_file.namelist() - bundle_root_dir = self._get_bundle_root_dir(file_names) - bundle_path = os.path.join(bundle_dir, bundle_root_dir) - - if os.spawnlp(os.P_WAIT, 'unzip', 'unzip', self._path, '-d', bundle_dir): - raise ZipExtractException - - self._init_with_path(bundle_path) - - if not activity.get_registry().add_bundle(bundle_path): - raise RegistrationException - - def deinstall(self): - if not self.is_installed(): - raise NotInstalledException - - ext = os.path.splitext(self._path)[1] - if not os.path.isfile(self._path) or ext != '.activity': - raise InvalidPathException - - for root, dirs, files in os.walk(self._path, topdown=False): - for name in files: - os.remove(os.path.join(root, name)) - for name in dirs: - os.rmdir(os.path.join(root, name)) - os.rmdir(self._path) - - self._init_with_path(None) - - # TODO: notify shell - diff --git a/sugar/activity/bundlebuilder.py b/sugar/activity/bundlebuilder.py index d2b1a89..a1f6c4b 100644 --- a/sugar/activity/bundlebuilder.py +++ b/sugar/activity/bundlebuilder.py @@ -24,7 +24,7 @@ import re import gettext from sugar import env -from sugar.activity.bundle import Bundle +from sugar.bundle.activitybundle import ActivityBundle class _SvnFileList(list): def __init__(self): @@ -98,7 +98,7 @@ def _get_install_dir(prefix): return os.path.join(prefix, 'share/activities') def _get_package_name(bundle_name): - bundle = Bundle(_get_source_path()) + bundle = ActivityBundle(_get_source_path()) zipname = '%s-%d.xo' % (bundle_name, bundle.get_activity_version()) return zipname @@ -108,7 +108,7 @@ def _delete_backups(arg, dirname, names): os.remove(os.path.join(dirname, name)) def _get_service_name(): - bundle = Bundle(_get_source_path()) + bundle = ActivityBundle(_get_source_path()) return bundle.get_service_name() def cmd_help(): diff --git a/sugar/bundle/Makefile.am b/sugar/bundle/Makefile.am index 2ad0958..f1af791 100644 --- a/sugar/bundle/Makefile.am +++ b/sugar/bundle/Makefile.am @@ -2,4 +2,5 @@ sugardir = $(pythondir)/sugar/bundle sugar_PYTHON = \ __init__.py \ bundle.py \ + activitybundle.py \ contentbundle.py diff --git a/sugar/bundle/activitybundle.py b/sugar/bundle/activitybundle.py new file mode 100644 index 0000000..07a5255 --- /dev/null +++ b/sugar/bundle/activitybundle.py @@ -0,0 +1,248 @@ +# 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""" + +from ConfigParser import ConfigParser +import locale +import os +import tempfile + +from sugar.bundle.bundle import Bundle, MalformedBundleException +from sugar import activity +from sugar import env + +_PYTHON_FACTORY='sugar-activity-factory' + +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._icon = None + self._service_name = None + self._mime_types = None + self._show_launcher = True + self._activity_version = 0 + + 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) + + def _parse_info(self, info_file): + cp = ConfigParser() + cp.readfp(info_file) + + section = 'Activity' + + if cp.has_option(section, 'service_name'): + self._service_name = cp.get(section, 'service_name') + else: + raise MalformedBundleException( + 'Activity bundle %s does not specify a service name' % + 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) + + if cp.has_option(section, 'class'): + 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') + self._mime_types = mime_list.strip(';').split(';') + + if cp.has_option(section, 'show_launcher'): + if cp.get(section, 'show_launcher') == 'no': + self._show_launcher = False + + 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: + self._activity_version = int(version) + except ValueError: + raise MalformedBundleException( + 'Activity bundle %s has invalid version number %s' % + (self._path, 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._name = cp.get(section, 'name') + + def get_locale_path(self): + """Get the locale path inside the (installed) activity bundle.""" + if not self._unpacked: + raise NotInstalledException + return os.path.join(self._path, 'locale') + + def get_icons_path(self): + """Get the icons path inside the (installed) activity bundle.""" + if not self._unpacked: + 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._name + + def get_service_name(self): + """Get the activity service name""" + return self._service_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 + def get_icon(self): + """Get the activity icon name""" + icon_path = os.path.join('activity', self._icon + '.svg') + if self._unpacked: + return os.path.join(self._path, icon_path) + else: + icon_data = self._get_file(icon_path).read() + temp_file, temp_file_path = tempfile.mkstemp(self._icon) + os.write(temp_file, icon_data) + os.close(temp_file) + return 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.join(self._path, self.bundle_exec) + command = command.replace('$SUGAR_BUNDLE_PATH', self._path) + command = os.path.expandvars(command) + else: + command = '%s --bundle-path="%s"' % ( + env.get_bin_path(_PYTHON_FACTORY), self._path) + + return command + + + def get_mime_types(self): + """Get the MIME types supported by the activity""" + return self._mime_types + + def get_show_launcher(self): + """Get whether there should be a visible launcher for the activity""" + return self._show_launcher + + def is_installed(self): + if activity.get_registry().get_activity(self._service_name): + return True + else: + return False + + def install(self): + if self.is_installed(): + raise AlreadyInstalledException + + install_dir = env.get_user_activities_path() + self._unzip(install_dir) + + install_path = os.path.join(install_dir, self._zip_root_dir) + if not activity.get_registry().add_bundle(install_path): + raise RegistrationException + + 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._service_name) + os.symlink(mime_path, installed_mime_path) + os.spawnlp(os.P_WAIT, 'update-mime-database', + 'update-mime-database', mime_dir) + + icons_dir = os.path.join(install_path, 'activity', 'mime-icons') + if os.path.isdir(icons_dir): + 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 file in os.listdir(icons_dir): + if file.endswith('.svg') or file.endswith('.icon'): + os.symlink(os.path.join(icons_dir, file), + os.path.join(installed_icons_dir, file)) + + def uninstall(self): + if not self.is_installed(): + raise NotInstalledException + + self._uninstall() + # FIXME: notify shell + diff --git a/sugar/datastore/datastore.py b/sugar/datastore/datastore.py index 48a3e52..0c2379d 100644 --- a/sugar/datastore/datastore.py +++ b/sugar/datastore/datastore.py @@ -24,9 +24,10 @@ import gobject from sugar.datastore import dbus_helpers from sugar import activity -from sugar.activity.bundle import Bundle from sugar.activity.activityhandle import ActivityHandle from sugar.bundle.contentbundle import ContentBundle +from sugar.bundle.activitybundle import ActivityBundle +from sugar.bundle.contentbundle import ContentBundle from sugar.objects import mime class DSMetadata(gobject.GObject): @@ -121,22 +122,24 @@ class DSObject(object): return activities + def is_activity_bundle(self): + return self.metadata['mime_type'] in \ + [ActivityBundle.MIME_TYPE, ActivityBundle.DEPRECATED_MIME_TYPE] + def is_content_bundle(self): return self.metadata['mime_type'] == ContentBundle.MIME_TYPE - # FIXME: should become is_activity_bundle() def is_bundle(self): - return self.metadata['mime_type'] in ['application/vnd.olpc-x-sugar', - 'application/vnd.olpc-sugar'] + return self.is_activity_bundle() or self.is_content_bundle() def resume(self, service_name=None): from sugar.activity import activityfactory - if self.is_bundle(): + if self.is_activity_bundle(): if service_name is not None: raise ValueError('Object is a bundle, cannot be resumed as an activity.') - bundle = Bundle(self.file_path) + bundle = ActivityBundle(self.file_path) if not bundle.is_installed(): bundle.install() diff --git a/sugar/graphics/objectchooser.py b/sugar/graphics/objectchooser.py index 59c77af..114665f 100644 --- a/sugar/graphics/objectchooser.py +++ b/sugar/graphics/objectchooser.py @@ -22,7 +22,7 @@ from gettext import gettext as _ import gtk import hippo -from sugar.activity.bundle import Bundle +from sugar.bundle.activitybundle import ActivityBundle from sugar.graphics import style from sugar.graphics.icon import CanvasIcon from sugar.graphics.xocolor import XoColor @@ -136,8 +136,8 @@ class CollapsedEntry(CanvasRoundBox): if self._icon_name: return self._icon_name - if self.jobject.is_bundle(): - bundle = Bundle(self.jobject.file_path) + if self.jobject.is_activity_bundle(): + bundle = ActivityBundle(self.jobject.file_path) self._icon_name = bundle.get_icon() if self.jobject.metadata['activity']: -- cgit v0.9.1