# 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)