Note: this patch does not expose the fix_manifest function when running bundlebuilder as a script. That would be a 2 or 3 line patch, seperately.<br><br>From 9ab619888a9d4ef40ca3e984d4485c5a22558105 Mon Sep 17 00:00:00 2001<br>
From: chema <chema@chema-laptop.(none)><br>Date: Fri, 6 Jun 2008 10:01:21 -0600<br>Subject: [PATCH] Bundlebuilder to use MANIFEST, fix_manifest function, some functions<br>pushed upstream to bundle.py and activitybundle.py<br>
---<br> src/sugar/activity/bundlebuilder.py | 69 ++++++++++--------<br> src/sugar/bundle/activitybundle.py | 140 ++++++++++++++++++++++++++++++++---<br> src/sugar/bundle/bundle.py | 23 ++++++<br> 3 files changed, 189 insertions(+), 43 deletions(-)<br>
<br>diff --git a/src/sugar/activity/bundlebuilder.py b/src/sugar/activity/bundlebuilder.py<br>index 1063f72..72c246c 100644<br>--- a/src/sugar/activity/bundlebuilder.py<br>+++ b/src/sugar/activity/bundlebuilder.py<br>@@ -19,28 +19,16 @@ import os<br>
import zipfile<br> import tarfile<br> import shutil<br>-import subprocess<br>+from subprocess import Popen, PIPE<br> import re<br> import gettext<br> from optparse import OptionParser<br>+import warnings<br>+import subprocess<br>
<br> from sugar import env<br>-from sugar.bundle.activitybundle import ActivityBundle<br>-<br>-def list_files(base_dir, ignore_dirs=None, ignore_files=None):<br>- result = []<br>+from sugar.bundle.activitybundle import ActivityBundle,MANIFEST,list_files<br>
<br>- for root, dirs, files in os.walk(base_dir):<br>- for f in files:<br>- if ignore_files and f not in ignore_files:<br>- rel_path = root[len(base_dir) + 1:]<br>- result.append(os.path.join(rel_path, f))<br>
- if ignore_dirs and root == base_dir:<br>- for ignore in ignore_dirs:<br>- if ignore in dirs:<br>- dirs.remove(ignore)<br>-<br>- return result<br> <br> class Config(object):<br>
def __init__(self, bundle_name, source_dir=None):<br>@@ -53,6 +41,7 @@ class Config(object):<br> bundle = ActivityBundle(self.source_dir)<br> version = bundle.get_activity_version()<br> <br>+ self.bundle = bundle<br>
self.bundle_name = bundle_name<br> self.xo_name = '%s-%d.xo' % (self.bundle_name, version)<br> self.tarball_name = '%s-%d.tar.bz2' % (self.bundle_name, version)<br>@@ -103,9 +92,9 @@ class Builder(object):<br>
f.close()<br> <br> class Packager(object):<br>- def __init__(self, config):<br>+ def __init__(self, config, dist_dir = None):<br> self.config = config<br>- self.dist_dir = os.path.join(self.config.source_dir, 'dist')<br>
+ self.dist_dir = dist_dir or os.path.join(self.config.source_dir, 'dist')<br> self.package_path = None<br> <br> if not os.path.exists(self.dist_dir):<br>@@ -113,19 +102,28 @@ class Packager(object):<br>
<br> <br> class BuildPackager(Packager):<br>- def __init__(self, config):<br>- Packager.__init__(self, config)<br>+ def __init__(self, config,dist_dir = None):<br>+ Packager.__init__(self, config,dist_dir )<br>
self.build_dir = self.config.source_dir<br> <br> def get_files(self):<br>- return list_files(self.build_dir,<br>- ignore_dirs=['po', 'dist', '.git'],<br>
- ignore_files=['.gitignore'])<br>+ return self.config.bundle.get_files()<br>+ <br>+ def fix_manifest(self):<br>+ allfiles = list_files(self.build_dir,<br>+ ignore_dirs=['dist', '.git'],<br>
+ ignore_files=['.gitignore', 'MANIFEST', '*.pyc', '*~', '*.bak'])<br>+ for file in allfiles:<br>+ if file not in self.config.bundle.manifest:<br>
+ self.config.bundle.manifest.append(file)<br>+ manifestfile = open(os.path.join(self.config.source_dir,MANIFEST),"wb")<br>+ for line in self.config.bundle.manifest:<br>+ manifestfile.write(line + "\n")<br>
<br> class XOPackager(BuildPackager):<br>- def __init__(self, config):<br>+ def __init__(self, config,dist_dir = None,dist_name = None):<br> BuildPackager.__init__(self, config)<br>- self.package_path = os.path.join(self.dist_dir, self.config.xo_name)<br>
+ self.package_path = os.path.join(self.dist_dir, dist_name or self.config.xo_name)<br> <br> def package(self):<br> bundle_zip = zipfile.ZipFile(self.package_path, 'w',<br>@@ -137,16 +135,25 @@ class XOPackager(BuildPackager):<br>
<br> bundle_zip.close()<br> <br>-class SourcePackager(Packager):<br>- def __init__(self, config):<br>- Packager.__init__(self, config)<br>+class SourcePackager(BuildPackager):<br>+ def __init__(self, config,dist_dir = None):<br>
+ BuildPackager.__init__(self, config,dist_dir)<br> self.package_path = os.path.join(self.dist_dir,<br> self.config.tarball_name)<br> <br> def get_files(self):<br>
- return list_files(self.config.source_dir,<br>- ignore_dirs=['locale', 'dist', '.git'],<br>- ignore_files=['.gitignore'])<br>+ git_ls = Popen('git-ls-files',stdout=PIPE,cwd=self.config.source_dir)<br>
+ if git_ls.wait():<br>+ #non-0 return code - failed<br>+ return Packager.get_files(self)<br>+ f = git_ls.stdout<br>+ files = []<br>+ for line in f.readlines():<br>+ filename = line.strip()<br>
+ if not filename.startswith('.'):<br>+ files.append(filename)<br>+ f.close()<br>+ return files<br> <br> def package(self):<br> tar = tarfile.open(self.package_path, "w")<br>
diff --git a/src/sugar/bundle/activitybundle.py b/src/sugar/bundle/activitybundle.py<br>index db30555..1ff7d39 100644<br>--- a/src/sugar/bundle/activitybundle.py<br>+++ b/src/sugar/bundle/activitybundle.py<br>@@ -21,15 +21,31 @@ from ConfigParser import ConfigParser<br>
import locale<br> import os<br> import tempfile<br>+from fnmatch import fnmatch<br> <br> from sugar.bundle.bundle import Bundle, MalformedBundleException, \<br> AlreadyInstalledException, RegistrationException, \<br>
NotInstalledException<br> <br>-from sugar import activity<br>-from sugar import env<br>-<br> import logging<br>+import warnings<br>+<br>+MANIFEST = "MANIFEST"<br>+<br>+def list_files(base_dir, ignore_dirs=None, ignore_files=None):<br>
+ result = []<br>+ <br>+ for root, dirs, files in os.walk(base_dir):<br>+ for f in files:<br>+ if not (ignore_files and [True for pat in ignore_files if fnmatch(f,pat)]):<br>+ #result matches a pattern in ignore_files, ignore it<br>
+ rel_path = root[len(base_dir) + 1:]<br>+ result.append(os.path.join(rel_path, f))<br>+ if ignore_dirs and root == base_dir:<br>+ for ignore in ignore_dirs:<br>+ if ignore in dirs:<br>
+ dirs.remove(ignore)<br>+ return result<br> <br> class ActivityBundle(Bundle):<br> """A Sugar activity bundle<br>@@ -64,7 +80,62 @@ class ActivityBundle(Bundle):<br> linfo_file = self._get_linfo_file()<br>
if linfo_file:<br> self._parse_linfo(linfo_file)<br>-<br>+ <br>+ self.read_manifest()<br>+<br>+ def _raw_manifest(self):<br>+ try:<br>+ f = self._get_file(MANIFEST)<br>
+ except IOError:<br>+ f = none<br>+ if not f:<br>+ warnings.warn(MalformedBundleException("Activity directory lacks a MANIFEST file."))<br>+ return []<br>+ ret = map(lambda x:x.strip(),f.readlines()) #strip trailing \n and other whitespace<br>
+ f.close()<br>+ return ret<br>+ <br>+ def read_manifest(self):<br>+ """read_manifest: sets self.manifest to list of lines in MANIFEST, <br>+ with invalid lines replaced by empty lines.<br>
+ <br>+ Since absolute order carries information on file history, it should be preserved.<br>+ For instance, when renaming a file, you should leave the new name on the same line<br>+ as the old one.<br>
+ """<br>+ manifestlines = self._raw_manifest()<br>+ for num,line in enumerate(manifestlines):<br>+ if line:<br>+ if line in manifestlines[0:num]:<br>+ manifestlines[num] = ""<br>
+ warnings.warn(MalformedBundleException("Bundle %s: duplicate entry in MANIFEST: %s"<br>+ %(self._name,line)))<br>+ continue<br>
+ if line == MANIFEST:<br>+ manifestlines[num] = ""<br>+ warnings.warn(MalformedBundleException("Bundle %s: MANIFEST includes itself: %s"<br>+ %(self._name,line)))<br>
+ if line.endswith("/"):<br>+ if not self._is_dir(line):<br>+ manifestlines[num] = ""<br>+ warnings.warn(MalformedBundleException("Bundle %s: invalid dir entry in MANIFEST: %s"<br>
+ %(self._name,line)))<br>+ else:<br>+ if not self._is_file(line):<br>+ manifestlines[num] = ""<br>+ warnings.warn(MalformedBundleException("Bundle %s: invalid entry in MANIFEST: %s"<br>
+ %(self._name,line)))<br>+ #remove trailing newlines - unlike internal newlines, they do not help keep absolute position<br>+ while manifestlines[-1]=="":<br>
+ manifestlines = manifestlines[:-1]<br>+ self.manifest = manifestlines<br>+ <br>+ def get_files(self,manifest = None):<br>+ return [MANIFEST] + filter(lambda line: line and not line.endswith("/"),manifest or self.manifest)<br>
+ <br>+ def get_dirs(self):<br>+ return filter(lambda line: line and line.endswith("/"),self.manifest)<br>+ <br> def _parse_info(self, info_file):<br> cp = ConfigParser()<br> cp.readfp(info_file)<br>
@@ -116,6 +187,7 @@ class ActivityBundle(Bundle):<br> raise MalformedBundleException(<br> 'Activity bundle %s has invalid version number %s' %<br> (self._path, version))<br>
+ <br> <br> def _get_linfo_file(self):<br> lang = locale.getdefaultlocale()[0]<br>@@ -209,28 +281,53 @@ class ActivityBundle(Bundle):<br> return self._show_launcher<br> <br> def is_installed(self):<br>
+ from sugar import activity<br>+ <br> if activity.get_registry().get_activity(self._bundle_id):<br> return True<br> else:<br> return False<br> <br> def need_upgrade(self):<br>
+ from sugar import activity<br>+ <br> act = activity.get_registry().get_activity(self._bundle_id)<br> if act is None or act.version != self._activity_version:<br> return True<br>
else:<br> return False<br>-<br>- def install(self):<br>- activities_path = env.get_user_activities_path()<br>- act = activity.get_registry().get_activity(self._bundle_id)<br>- if act is not None and act.path.startswith(activities_path):<br>
- raise AlreadyInstalledException<br>-<br>- install_dir = env.get_user_activities_path()<br>+ <br>+ def unpack(self,install_dir,strict_manifest=False):<br> self._unzip(install_dir)<br> <br>
install_path = os.path.join(install_dir, self._zip_root_dir)<br>+ <br>+ #check installed files against the MANIFEST<br>+ manifestfiles = self.get_files(self._raw_manifest())<br>+ for file in list_files(install_path):<br>
+ if file in manifestfiles:<br>+ manifestfiles.remove(file)<br>+ elif file != MANIFEST:<br>+ #warnings.warn(MalformedBundleException("Bundle %s: %s not in MANIFEST"%<br>
+ # (self._name,file)))<br>+ if strict_manifest:<br>+ os.remove(os.path.join(install_path,file))<br>+ if manifestfiles:<br>+ err = MalformedBundleException("Bundle %s: files in MANIFEST not included: %s"%<br>
+ (self._name,str(manifestfiles)))<br>+ if strict_manifest:<br>+ raise err<br>+ else:<br>+ warnings.warn(err)<br>+ <br>
+ #create empty directories<br>+ for dir in self.get_dirs():<br>+ dirpath = os.path.join(install_path,dir)<br>+ if os.path.isdir(dirpath):<br>+ warnings.warn(MalformedBundleException("Bunldle %s: non-empty dir %s in MANIFEST"%<br>
+ (self._name,dirpath)))<br>+ else:<br>+ os.makedirs(os.path.join(install_path,dir))<br> <br> xdg_data_home = os.getenv('XDG_DATA_HOME',<br>
os.path.expanduser('~/.local/share'))<br>@@ -267,11 +364,27 @@ class ActivityBundle(Bundle):<br> os.symlink(info_file,<br> os.path.join(installed_icons_dir,<br>
os.path.basename(info_file)))<br>+ return install_path<br> <br>+ def install(self):<br>+ from sugar import activity<br>+ from sugar import env<br>+ <br>
+ activities_path = env.get_user_activities_path()<br>+ act = activity.get_registry().get_activity(self._bundle_id)<br>+ if act is not None and act.path.startswith(activities_path):<br>+ raise AlreadyInstalledException<br>
+<br>+ install_dir = env.get_user_activities_path()<br>+ install_path = self.unpack(install_dir)<br>+ <br> if not activity.get_registry().add_bundle(install_path):<br> raise RegistrationException<br>
<br> def uninstall(self, force=False):<br>+ from sugar import activity<br>+ from sugar import env<br>+ <br> if self._unpacked:<br> install_path = self._path<br> else:<br>
@@ -316,6 +429,9 @@ class ActivityBundle(Bundle):<br> raise RegistrationException<br> <br> def upgrade(self):<br>+ from sugar import activity<br>+ from sugar import env<br>+ <br> act = activity.get_registry().get_activity(self._bundle_id)<br>
if act is None:<br> logging.warning('Activity not installed')<br>diff --git a/src/sugar/bundle/bundle.py b/src/sugar/bundle/bundle.py<br>index 47d08b2..44e30b0 100644<br>--- a/src/sugar/bundle/bundle.py<br>
+++ b/src/sugar/bundle/bundle.py<br>@@ -114,6 +114,29 @@ class Bundle:<br> zip_file.close()<br> <br> return f<br>+ <br>+ def _is_dir(self, filename):<br>+ if self._unpacked:<br>+ path = os.path.join(self._path, filename)<br>
+ return os.path.isdir(path)<br>+ else:<br>+ return True #zip files contain all dirs you care about!<br>+ <br>+ def _is_file(self, filename):<br>+ if self._unpacked:<br>+ path = os.path.join(self._path, filename)<br>
+ return os.path.isfile(path)<br>+ else:<br>+ zip_file = zipfile.ZipFile(self._path)<br>+ path = os.path.join(self._zip_root_dir, filename)<br>+ try:<br>+ data = zip_file.getinfo(path)<br>
+ return True<br>+ except KeyError:<br>+ return False<br>+ finally:<br>+ zip_file.close()<br>+ <br> <br> def get_path(self):<br> """Get the bundle path."""<br>
-- <br><a href="http://1.5.2.5">1.5.2.5</a><br><br><br>