From eaef567cabc5c0dda9706faa0e17e20909131ade Mon Sep 17 00:00:00 2001 From: simpoir Date: Fri, 30 Jan 2009 04:14:48 +0000 Subject: ajout initial du tree jhbuild [jhbuild base] --- (limited to 'src/sugar/bundle') diff --git a/src/sugar/bundle/Makefile.am b/src/sugar/bundle/Makefile.am new file mode 100644 index 0000000..f1af791 --- /dev/null +++ b/src/sugar/bundle/Makefile.am @@ -0,0 +1,6 @@ +sugardir = $(pythondir)/sugar/bundle +sugar_PYTHON = \ + __init__.py \ + bundle.py \ + activitybundle.py \ + contentbundle.py diff --git a/src/sugar/bundle/__init__.py b/src/sugar/bundle/__init__.py new file mode 100644 index 0000000..85ebced --- /dev/null +++ b/src/sugar/bundle/__init__.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. diff --git a/src/sugar/bundle/activitybundle.py b/src/sugar/bundle/activitybundle.py new file mode 100644 index 0000000..eb35307 --- /dev/null +++ b/src/sugar/bundle/activitybundle.py @@ -0,0 +1,375 @@ +# 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 tempfile +import logging + +from sugar import env +from sugar import util +from sugar.bundle.bundle import Bundle, \ + MalformedBundleException, NotInstalledException + +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._bundle_id = None + self._mime_types = None + self._show_launcher = True + self._activity_version = 0 + self._installation_time = os.stat(path).st_mtime + self._manifest = None + + 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 _get_manifest(self): + if self._manifest is None: + self._manifest = self._read_manifest() + return self._manifest + + manifest = property(_get_manifest, None, None, + "NOTICE: this property is potentially quite slow, so better make sure " + "that it's not called at performance-critical points like shell or " + "activity startup.") + + def _raw_manifest(self): + f = self.get_file("MANIFEST") + if not f: + logging.warning("Activity directory lacks a MANIFEST file.") + return [] + + ret = [line.strip() for line in f.readlines()] + f.close() + return ret + + def _read_manifest(self): + """return a list with the lines in MANIFEST, with invalid lines replaced + by empty lines. + + Since absolute order carries information on file history, it should + be preserved. For instance, when renaming a file, you should leave + the new name on the same line as the old one. + """ + logging.debug('STARTUP: Reading manifest') + lines = self._raw_manifest() + + # Remove trailing newlines, they do not help keep absolute position. + while lines and lines[-1] == "": + lines = lines[:-1] + + for num, line in enumerate(lines): + if not line: + continue + + # Remove duplicates + if line in lines[0:num]: + lines[num] = "" + logging.warning("Bundle %s: duplicate entry in MANIFEST: %s" + % (self._name,line)) + continue + + # Remove MANIFEST + if line == "MANIFEST": + lines[num] = "" + logging.warning("Bundle %s: MANIFEST includes itself: %s" + % (self._name,line)) + + # Remove invalid files + if not self.is_file(line): + lines[num] = "" + logging.warning("Bundle %s: invalid entry in MANIFEST: %s" + % (self._name,line)) + + return lines + + def get_files(self, manifest = None): + files = [line for line in (manifest or self.manifest) if line] + + if self.is_file('MANIFEST'): + files.append('MANIFEST') + + return files + + 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'): + 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'): + 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, '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 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._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 + + # 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._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(self._icon) + 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_show_launcher(self): + """Get whether there should be a visible launcher for the activity""" + return self._show_launcher + + def install(self, install_dir=None, strict_manifest=False): + 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) + + # List installed files + manifestfiles = self.get_files(self._raw_manifest()) + paths = [] + for root, dirs_, files in os.walk(install_path): + rel_path = root[len(install_path) + 1:] + for f in files: + paths.append(os.path.join(rel_path, f)) + + # Check the list against the MANIFEST + for path in paths: + if path in manifestfiles: + manifestfiles.remove(path) + elif path != "MANIFEST": + logging.warning("Bundle %s: %s not in MANIFEST"% + (self._name,path)) + if strict_manifest: + os.remove(os.path.join(install_path, path)) + + # Is anything in MANIFEST left over after accounting for all files? + if manifestfiles: + err = ("Bundle %s: files in MANIFEST not included: %s"% + (self._name,str(manifestfiles))) + if strict_manifest: + raise MalformedBundleException(err) + else: + logging.warning(err) + + 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) + os.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' + if os.path.isfile(svg_file): + os.symlink(svg_file, + os.path.join(installed_icons_dir, + os.path.basename(svg_file))) + if os.path.isfile(info_file): + os.symlink(info_file, + os.path.join(installed_icons_dir, + os.path.basename(info_file))) + return install_path + + def uninstall(self, install_path, force=False): + 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) + + self._uninstall(install_path) diff --git a/src/sugar/bundle/bundle.py b/src/sugar/bundle/bundle.py new file mode 100644 index 0000000..a1b2686 --- /dev/null +++ b/src/sugar/bundle/bundle.py @@ -0,0 +1,199 @@ +# 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 + + if os.path.isdir(self._path): + self._zip_file = None + else: + self._zip_file = zipfile.ZipFile(self._path) + self._check_zip_bundle() + + # manifest = self._get_file(self._infodir + '/contents') + # if manifest is None: + # raise MalformedBundleException('No manifest file') + # + # signature = self._get_file(self._infodir + '/contents.sig') + # if signature is None: + # raise MalformedBundleException('No signature file') + + 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 "%s"' % + 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. + # FIXME: use manifest + 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) diff --git a/src/sugar/bundle/contentbundle.py b/src/sugar/bundle/contentbundle.py new file mode 100644 index 0000000..a95ed61 --- /dev/null +++ b/src/sugar/bundle/contentbundle.py @@ -0,0 +1,220 @@ +# 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 content bundles + +UNSTABLE. +""" + +from ConfigParser import ConfigParser +import os +import urllib + +from sugar import env +from sugar.bundle.bundle import Bundle, NotInstalledException, \ + MalformedBundleException + +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 = None + self._bundle_class = None + self._activity_start = 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, 'host_version'): + version = cp.get(section, 'host_version') + try: + if int(version) != 1: + raise MalformedBundleException( + 'Content bundle %s has unknown host_version number %s' % + (self._path, version)) + except ValueError: + raise MalformedBundleException( + 'Content bundle %s has invalid host_version number %s' % + (self._path, version)) + + 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: + self._library_version = int(version) + except ValueError: + raise MalformedBundleException( + 'Content bundle %s has invalid version number %s' % + (self._path, 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 "%s"' % + (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, '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' + + 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 is_installed(self): + if self._zip_file is None: + return True + elif os.path.isdir(self.get_root_dir()): + return True + else: + return False + + def install(self): + self._unzip(env.get_user_library_path()) + self._run_indexer() + + 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() -- cgit v0.9.1