Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/project.py
blob: e41628e71ed7fc21d20e26fd59548f34a7a65b3f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
#!/usr/bin/env python
# Copyright (C) 2009, George Hunt <georgejhunt@gmail.com>
# Copyright (C) 2009, One Laptop Per Child
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
from __future__ import with_statement
import os, os.path, ConfigParser, shutil, sys
from subprocess import Popen, PIPE

from gettext import gettext as _

#major packages
import gtk
import time
import datetime
import gobject
from fnmatch import fnmatch

#sugar stuff
import sugar.env
from sugar.datastore import datastore
from sugar.graphics.alert import *
import sugar.activity.bundlebuilder as bundlebuilder
#build 650 doesn't include fix_manifest
#import bundlebuilder
from sugar.bundle.activitybundle import ActivityBundle

#following only works in sugar 0.82
#from sugar.activity.registry import get_registry
from sugar.activity.activity import Activity
from sugar import profile

#import logging
from  pydebug_logging import _logger, log_environment, log_dict

class ProjectFunctions:
    
    def __init__(self,activity):
        self._activity = activity
        self._load_to_playpen_source = None

    def get_editor(self):
        raise NotImplimentedError
        
    def write_binary_to_datastore(self):
        """
        Check to see if there is a child loaded.
        then bundle it up and write it to the journal
        lastly serialize the project information and write it to the journal
        """
        if self._activity.child_path == None: return
        dist_dir = os.path.join(self._activity.child_path,'dist')
        try:
            os.rmtree(dist_dir)
        except:
            _logger.debug('failed to rmtree %s'%dist_dir)
        try:
            os.mkdir(dist_dir)
        except:
            _logger.debug('failed to os.mkdir %s'%dist_dir)
        
        #are there changes in edit buffers not flushed to disk?
        self.get_editor().save_all()

        #remove any embeded shell breakpoints
        self.get_editor().clear_embeds()
        
        #create the manifest for the bundle
        manifest_ok = self.write_manifest()
        
        #see if this bundle passes the parse test
        bundle = ActivityBundle(self._activity.child_path)
        
        do_tgz = True
        mime = self._activity.MIME_ZIP
        activity = 'org.laptop.PyDebug'
        #if manifest was successful, write the xo bundle to the instance directory
        if bundle:
            do_tgz = False
            try:                
                #actually write the xo file
                if self._activity.sugar_minor >= 82:
                    _logger.debug('making xo bundle from: %s'%(self._activity.child_path))
                    config = bundlebuilder.Config(self._activity.child_path)
                    packager = bundlebuilder.XOPackager(bundlebuilder.Builder(config))
                    packager.package()
                    xo_name = config.xo_name
                    source = os.path.join('dist', xo_name)
                else:
                    #how to create the zipped xo file on build 650
                    name = self._activity.activity_dict.get('name','')
                    xo_name = bundlebuilder._get_package_name(name)
                    _logger.debug('zipped xo name:%s current dir: %s'%(xo_name, os.getcwd(),))
                    bundlebuilder.cmd_dist(name, 'MANIFEST')
                    source = xo_name
                    
                dest = os.path.join(self._activity.get_activity_root(),
                                    'instance', xo_name)
                _logger.debug('writing to the journal from %s to %s.'%(source,dest))
                if os.path.isfile(dest):
                    os.unlink(dest)
                try:
                    package = xo_name
                    shutil.copy(source,dest)
                    mime = self._activity.MIME_TYPE
                    activity = self._activity.activity_dict.get('activity','')
                    self.to_removable_bin(source)
                except IOError:
                    _logger.debug('shutil.copy error %d: %s. ',IOError[0],IOError[1])
                    do_tgz = True
                    mime = self._activity.MIME_ZIP
            except Exception, e:
                _logger.exception('outer exception %r'%e)
                do_tgz = True
        else:
            _logger.debug('unable to parse bundle with ActivityBundle object')
        if do_tgz:
            dest = self.just_do_tar_gz()
            if dest:
                package = os.path.basename(dest)                
        dsobject = datastore.create()
        dsobject.metadata['package'] = package
        dsobject.metadata['title'] = package  
        dsobject.metadata['mime_type'] = mime
        dsobject.metadata['icon'] = self._activity.activity_dict.get('icon','')
        dsobject.metadata['bundle_id'] = self._activity.activity_dict.get('bundle_id','')
        dsobject.metadata['activity'] = activity
        dsobject.metadata['version'] = self._activity.activity_dict.get('version',1) 
        #calculate and store the new md5sum
        dsobject.metadata['tree_md5'] = self.save_tree_md5(self._activity.child_path)
        self._activity.debug_dict['tree_md5'] = dsobject.metadata['tree_md5']
        if dest: dsobject.set_file_path(dest)
        
        #actually make the call which writes to the journal
        try:
            datastore.write(dsobject,transfer_ownership=True)
            _logger.debug('succesfully wrote to the journal from %s.'%(dest))
        except Exception, e:
            _logger.error('datastore.write exception %r'%e)
            return
        #update the project display
        if self.journal_class: 
            self.journal_class.new_directory()
        
    def save_tree_md5(self, path):
        #calculate and store the new md5sum
        self._activity.debug_dict['tree_md5'] = self._activity.util.md5sum_tree(self._activity.child_path)
        return self._activity.debug_dict['tree_md5']

    def removable_backup(self):
        """ if there is a pydebug folder in root directory of a USB or SD, this
        routine will copy the debugee source tree to a folder name which includes
        the datetime
        """
        rs = self.removable_storage()
        _logger.debug('removable storage %r'%rs)
        for dest in rs:
            root = os.path.join(dest,'pydebug')
            if os.path.isdir(root):  #there is a pydebug directory in the root of this device
                today = datetime.date.today()               
                name = self._activity.child_path.split('/')[-1].split(".")[0] + '-' + str(today)
                #change name if necessary to prevent collision 
                basename = self._activity.util.non_conflicting(root,name)
                try:
                    shutil.copytree(self._activity.child_path, os.path.join(root,basename))
                    self._activity.util.set_permissions(os.path.join(root,basename))
                except Exception, e:
                    _logger.error('copytree exception %r'%e)
    
    def to_removable_bin(self, source):
        for dest in self.removable_storage():
            root = os.path.join(dest,'bin')
            if not os.path.isdir(root):  #there is no bin directory in the root of this device
                try:
                    os.mkdir(root)
                except Exception, e:
                    _logger.debug('mkdir exception %r'%e)
            if os.path.isdir(root):
                basename = os.path.basename(source)
                target = os.path.join(root,basename)
                if os.path.isfile(target):
                    os.unlink(target)
                _logger.debug('copying %s to %s'%(source,target))
                shutil.copyfile(source,target)
    
    def removable_storage(self):
        """uses shell 'mount' cmd dto get mounted volumes
        needs to be tested on various builds
        """
        cmd = 'mount'
        ret = []
        resp, status = self._activity.util.command_line(cmd)
        if status == 0:
            for line in resp.split('\n'):
                chunks = line.split()
                if len(chunks) > 2:
                    if chunks[2].startswith('/media'):
                        if chunks[2] == '/media/Boot': continue
                        _logger.debug('mount point: %s'%chunks[2])
                        ret.append(chunks[2])
            return ret
        return None
    
    def write_manifest(self):
        """ use sugar routines to build a new MANIFEST file """
        IGNORE_DIRS = ['dist', '.git']
        IGNORE_FILES = ['.gitignore', 'MANIFEST', '*.pyc', '*~', '*.bak', 'pseudo.po']
        try:
            os.remove(os.path.join(self._activity.child_path,'MANIFEST'))
        except:
            pass
        dest = self._activity.child_path
        manifest = self.list_files(dest, IGNORE_DIRS, IGNORE_FILES)
        _logger.debug('Writing manifest to %s.'%(dest))
        try:
            """
            config = bundlebuilder.Config(dest)
            b = bundlebuilder.Builder(config)
            b.fix_manifest()
            """
            f = open(os.path.join(self._activity.child_path, "MANIFEST"), "wb")
            for line in manifest:
                f.write(line + "\n")
            f.close()
        except Exception, e:
            _logger.debug('fix manifest error: %s'%(e,))
            return False
        return True

    #following two functions lifted (slight mods) from bundlebuilder build 852
    def fix_manifest(self):

        
        f = open(os.path.join(self._activity.child_path, "MANIFEST"), "wb")
        for line in manifest:
            f.write(line + "\n")

    def list_files(self, base_dir, ignore_dirs=None, ignore_files=None):
        result = []
    
        base_dir = os.path.abspath(base_dir)
    
        for root, dirs, files in os.walk(base_dir):
            if ignore_files:
                for pattern in ignore_files:
                    files = [f for f in files if not fnmatch(f, pattern)]
                    
            rel_path = root[len(base_dir) + 1:]
            for f in files:
                result.append(os.path.join(rel_path, f))
    
            if ignore_dirs and root == base_dir:
                for ignore in ignore_dirs:
                    if ignore in dirs:
                        dirs.remove(ignore)
    
        return result

    def just_do_tar_gz(self):
        """
        tar and compress the child_path tree to the journal
        """
        name = self._activity.child_path.split('/')[-1].split(".")[0]+'.tar.gz'
        os.chdir(self._activity.activity_playpen)
        dest = os.path.join(self._activity.get_activity_root(),'instance',name)
        cmd = 'tar czf %s %s'%(dest,'./'+os.path.basename(self._activity.child_path))
        ans = self._activity.util.command_line(cmd)
        _logger.debug('cmd:%s'%cmd)
        if ans[1]!=0:
            return None
        return dest
        
    def load_activity_to_playpen(self,file_path):
        """loads from a disk tree"""
        self._new_child_path =  os.path.join(self._activity.activity_playpen,os.path.basename(file_path))
        _logger.debug('copying file for %s to %s'%(file_path,self._new_child_path))
        self._load_playpen(file_path)
        
    def try_to_load_from_journal(self,object_id):
        """
        loads a zipped XO or tar.gz application file (tar.gz if bundler cannot parse the activity.info file)
        """
        self.ds = datastore.get(object_id[0])
        if not self.ds:
            _logger.debug('failed to get datastore object with id:%s'%object_id[0])
            return
        dsdict=self.ds.get_metadata()
        file_name_from_ds = self.ds.get_file_path()
        project = dsdict.get('package','')
        mime_type = dsdict.get('mime_type')
        _logger.debug('load from journal, mime type: %s'%mime_type)        
        if not mime_type in [self._activity.MIME_TYPE, self._activity.MIME_ZIP,]:
            self._activity.util.alert(_('This journal item does not appear to be a zipped activity. Package:%s.'%project))
            self.ds.destroy()
            self.ds = None
            return
        filestat = os.stat(file_name_from_ds)         
        size = filestat.st_size
        _logger.debug('In try_to_load_from_journal. Object_id %s. File_path %s. Size:%s'%(object_id[0], file_name_from_ds, size))
        if mime_type == self._activity.MIME_TYPE:
            try:
                self._bundler = ActivityBundle(file_name_from_ds)
                name_with_blanks = self._bundler.get_name()
                name = ''
                for i in range(len(name_with_blanks)):
                    if name_with_blanks[i] == ' ': continue
                    name += name_with_blanks[i]
                self._activity.activity_dict['name'] = name
                iszip=True
                istar = False
            except:
                self._activity.util.alert('Error:  Malformed Activity Bundle')
                self.ds.destroy()
                self.ds = None
                return
        else:
            name = project.split('.')[0]
            #self.delete_after_load = os.path.abspath(file_name_from_ds,name)
            iszip = False
            istar = True
        self._new_child_path = os.path.join(self._activity.activity_playpen,name+'.activity')
        self._load_playpen(file_name_from_ds, iszip, istar)
        
    def _load_playpen(self,source_fn, iszip = False, istar=False):
        """entry point for both xo and file tree sources"""
        self._load_to_playpen_source = source_fn
        #need to pass parameters to continuation routine
        self.lp_iszip = iszip
        self.lp_istar = istar
        #if necessary clean up contents of playpen
        #has the tree md5 changed?
        if not self._activity.debug_dict.get('tree_md5','') == '':            
            tree_md5 = self._activity.util.md5sum_tree(self._activity.child_path)
            if tree_md5 and tree_md5 != self._activity.debug_dict['tree_md5']:
                action_prompt = _('Select OK to abandon changes to ') + \
                            os.path.basename(self._activity.child_path)
                self._activity.util.confirmation_alert(action_prompt, \
                        _('Changes have been made to the PyDebug work area.'), \
                        self.continue_loading_playpen_cb)
                return        
        self.continue_loading_playpen_cb(None, None)
            
    def continue_loading_playpen_cb(self, alert, confirmation):           
        self._unload_playpen()
        iszip = self.lp_iszip
        istar = self.lp_istar
        if self._load_to_playpen_source == None:
            #having done the clearing, just stop
            return
        if iszip:
            if self._activity.sugar_minor >= 84:
                self._bundler.install(self._activity.activity_playpen)
            else:
                #self._bundler.install()
                tmp_name = os.path.basename(self._new_child_path) + '.xo'
                dest = os.path.join(self._activity.activity_playpen, tmp_name)
                shutil.copy(self._load_to_playpen_source,dest)
                os.chdir(self._activity.activity_playpen)
                cmd = 'unzip -q %s'%dest
                _logger.debug('loading XO file with cmd %s'%cmd)
                rtn = self._activity.util.command_line(cmd)
                if rtn[1] != 0: return
                os.unlink(dest)

            if self.ds:
                self.ds.destroy()
            self.ds = None
        elif istar:
            dsdict = self.ds.get_metadata()
            project = dsdict.get('package','dummy.tar.gz')
            name = project.split('.')[0]
            dest = os.path.join(self._activity.activity_playpen,project)
            shutil.copy(self._load_to_playpen_source,dest)
            os.chdir(self._activity.activity_playpen)
            cmd = 'tar zxf %s'%dest
            _logger.debug('loading tar.gz with cmd %s'%cmd)
            rtn = self._activity.util.command_line(cmd)
            if rtn[1] != 0: return
            os.unlink(dest)
            if self.ds: self.ds.destroy()
            self.ds = None
        elif os.path.isdir(self._load_to_playpen_source):
            #shutil.copy dies if the target exists, so rmtree if target exists
            basename = self._load_to_playpen_source.split('/')[-1]
            if basename.endswith('.activity'):
                dest = self._new_child_path
            else:
                dest = os.path.join(self._activity.child_path,basename)
            if  os.path.isdir(dest):
                shutil.rmtree(dest,ignore_errors=True)
            #os.mkdir(dest)
            _logger.debug('dest:%s'%dest)
            _logger.debug('copying tree from %s to %s'%(self._load_to_playpen_source,dest))
            shutil.copytree(self._load_to_playpen_source,dest)
            _logger.debug('returned from copytree')
        elif os.path.isfile(self._load_to_playpen_source):
            source_basename = os.path.basename(self._load_to_playpen_source)
            #dest = os.path.join(self._activity.child_path,source_basename)
            dest = self._activity.child_path
            _logger.debug('file copy from %s to %s'%(self._load_to_playpen_source, dest,))
            shutil.copy(self._load_to_playpen_source,dest)
        self._activity.debug_dict['source_tree'] = self._load_to_playpen_source
        self._activity.child_path = self._new_child_path
        self.setup_new_activity()
        
   
        """The following code needs to be copied inline to any routine which overwrites playpen
        #has the tree md5 changed?
        tree_md5 = self._activity.util.md5sum_tree(self._activity.child_path)
        if tree_md5 != self._activity.debug_dict['tree_md5']:
            action_prompt = _('Select OK to abandon changes to ') + \
                            os.path.basename(self._activity.child_path)
            self._activity.util.confirmation_alert(action_prompt,
                                                   _('Changes have been made to the PyDebug work area.'),
                                                   self.continue_inline_cb)
        """
        
    def _unload_playpen(self, rmtree = True):                                           
        #IPython gets confused if path it knows about suddenly disappears
        #cmd = "cd '%s'\n"%self._activity.pydebug_path
        os.environ['HOME'] = self._activity.debugger_home
        cmd = "quit()\ngo\n"
        self._activity.feed_virtual_terminal(0,cmd)
        if self._activity.child_path and os.path.isdir(self._activity.child_path):
            self.abandon_changes = True
            #there is a on change call back to disable
            self._activity.debug_dict['tree_md5'] = ''
            self._activity.debug_dict['child_path'] = ''
            self.get_editor().remove_all()
            if rmtree:
                shutil.rmtree(self._activity.child_path)
            self.abandon_changes = False
            
            #check to see if the Activity directory has a link to playpen
            link_dir = os.path.join('/home/olpc/Activities',
                            os.path.basename(self._activity.child_path))
            if os.path.islink(link_dir):
                os.unlink(link_dir)
                
            
    def copy_tree(self,source,dest):
            if os.path.isdir(dest):
                try:
                    shutil.rmtree(dest)
                except Exception, error:
                    _logger.debug('rmtree exception %r'%error)
            try:
                shutil.copytree(source,dest)
                self._activity.util.set_permissions(dest)
            except Exception, error:
                _logger.debug('copytree exception %r'%error)

    
    def read_activity_info(self, path):
        """
        Fetch the activity info from Datastore object and Sugar system calls 
        """
        try:
            _logger.debug ('passed in file path: %s'%path)       
            bundle = ActivityBundle(path)
        except Exception,e:
            _logger.debug('exception %r'%e)
            #msg = _('%s not recognized by ActivityBundle parser. Does activity/activity.info exist?'%os.path.basename(path))
            #self._activity.util.alert(msg)
            self._activity.init_activity_dict()
            if self._activity.child_path and os.path.isdir(self._activity.child_path) and \
                                self._activity.child_path.endswith('.activity'):
                name = os.path.basename(path).split('.')[0]
                self._activity.activity_dict['name'] = name
                self._activity.activity_dict['bundle_id'] = 'org.laptop.'  + name               
                return  #maybe should issue an alert here
        self._activity.activity_dict['version'] = str(bundle.get_activity_version())
        
        ############################
        #note to myself -- Made a decision I'm now forced to live with:
        #bundle install appears to crunch blanks out of name when it installs
        ###########################
        
        name_with_blanks = bundle.get_name()
        name = ''
        for i in range(len(name_with_blanks)):
            if name_with_blanks[i] == ' ': continue
            name += name_with_blanks[i]
        self._activity.activity_dict['name'] = name

        self._activity.activity_dict['bundle_id'] = bundle.get_bundle_id()
        self._activity.activity_dict['command'] = bundle.get_command()
        cmd_args = bundle.get_command()
        self._activity.activity_dict['command'] = cmd_args
        if cmd_args.startswith('sugar-activity'):
            mod_class = cmd_args.split()[1]
            if '.' in mod_class:
                self._activity.activity_dict['class'] = mod_class.split('.')[1]  
                self._activity.activity_dict['module'] = mod_class.split('.')[0]
        else:
            self._activity.activity_dict['module'] = cmd_args
            self._activity.activity_dict['class'] = ''
        self._activity.activity_dict['icon'] = bundle.get_icon()
        self._activity.activity_dict['title'] = 'PyDebug_' + self._activity.activity_dict['name']
        log_dict(self._activity.activity_dict,'Contents of activity_dict')
        self._activity.update_metadata()