• Home
  • History
  • Annotate
  • Line#
  • Navigate
  • Raw
  • Download
  • only in /macosx-10.10.1/pyobjc-45/2.5/pyobjc/pyobjc-framework-Cocoa/Examples/AppKit/PackageManager/
1"""
2Cocoa GUI for the Package Manager
3
4This is a first generation of the Cocoa GUI, it inherits some of the nasty
5features of the current Carbon version:
6
71. GUI blocks during some operations, such as downloading or installing
8
92. Checking on GUI packages may crash the application
10
11The first item can only be solved by rewriting parts of the pimp module, the
12second part will be solved by running at least some pimp related code in a
13seperate process.
14
15TODO:
16- Make sure 'File -> Open...' actually works
17
18XXX:
19- save preferences in the favorites db (for databases that are in in there)?
20"""
21
22from Cocoa import *
23import objc
24import threading
25
26from PyObjCTools import AppHelper
27
28import sys
29import pimp
30import webbrowser
31
32# File type for packman databases
33DB_FILE_TYPE="Python Package Database"
34
35# Extract class information from the NIB files
36# - MainMenu: Global application stuff
37# - OpenPanel: The 'Open URL...' window
38# - PackageDatabase: Document window
39
40def setString(field, value):
41    """
42    Set an NSTextField to the specified value. Clears the field if 'value'
43    is None.
44    """
45    if value is None:
46        field.setStringValue_("")
47    else:
48        field.setStringValue_(value)
49
50
51##
52# We break the abstraction of some of the objects in the pimp module. That
53# is necessary because we cannot get at the required information using the
54# public interfaces :-(
55#
56def DB_DESCRIPTION(pimpDB):
57    return pimpDB._description
58
59def DB_MAINTAINER(pimpDB):
60    return pimpDB._maintainer
61
62def DB_URL(pimpDB):
63    return pimpDB._urllist[0]
64
65def PKG_HIDDEN(package):
66    """ Return True iff the package is a hidden package """
67    return (package._dict.get('Download-URL', None) is None)
68
69
70
71
72class PackageDatabase (NSDocument):
73    """
74    The document class for a package database
75    """
76    databaseMaintainer = objc.IBOutlet()
77    databaseName = objc.IBOutlet()
78    installButton = objc.IBOutlet()
79    installDependencies = objc.IBOutlet()
80    installationLocation = objc.IBOutlet()
81    installationLog = objc.IBOutlet()
82    installationPanel = objc.IBOutlet()
83    installationProgress = objc.IBOutlet()
84    installationTitle = objc.IBOutlet()
85    itemDescription = objc.IBOutlet()
86    itemHome = objc.IBOutlet()
87    itemInstalled = objc.IBOutlet()
88    itemStatus = objc.IBOutlet()
89    overwrite = objc.IBOutlet()
90    packageTable = objc.IBOutlet()
91    prerequisitesTable = objc.IBOutlet()
92    progressOK = objc.IBOutlet()
93    showHidden = objc.IBOutlet()
94    verbose = objc.IBOutlet()
95
96
97    def init(self):
98        """
99        Initialize the document without a database
100        """
101
102        self = super(PackageDatabase, self).init()
103        if self is None: return None
104        self.pimp =  None
105        self._packages = []
106        return self
107
108
109    def initWithContentsOfFile_ofType_(self, path, type):
110        """
111        Open a local database.
112        """
113        self = self.init()
114        if self is None: return self
115
116        url = NSURL.fileURLWithPath_(path)
117
118        self.openDB(url.absoluteString())
119        return self
120
121    def __del__(self):
122        """ Clean up after ourselves """
123        if hasattr(self, 'timer'):
124            self.timer.invalidate()
125            del self.timer
126
127    def close(self):
128        if hasattr(self, 'timer'):
129            self.timer.invalidate()
130            del self.timer
131        super(PackageDatabase, self).close()
132
133    def setDB(self, pimpURL, pimpDB):
134        self.pimp = pimpDB
135        self._packages = pimpDB.list()
136        self._prerequisites = []
137        if self.databaseName is not None:
138            self.databaseName.setStringValue_(DB_DESCRIPTION(self.pimp))
139            self.databaseMaintainer.setStringValue_(DB_MAINTAINER(self.pimp))
140
141        if self.packageTable is not None:
142            self.packageTable.reloadData()
143            self.tableViewSelectionDidChange_(None)
144
145        self.setFileName_(pimpURL)
146        self.pimpURL = pimpURL
147
148        if hasattr(self, 'timer'):
149            self.timer.invalidate()
150
151        self.timer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
152                10.0,
153                self,
154                self.checkUpdates_,
155                None,
156                True)
157
158
159    def openDB(self, dbUrl=None):
160        """
161        Open a database at the specified URL
162        """
163        prefs = pimp.PimpPreferences()
164        if dbUrl is not None:
165            prefs.pimpDatabase = dbUrl
166        else:
167            prefs.pimpDatabase = pimp.DEFAULT_PIMPDATABASE
168
169        db = pimp.PimpDatabase(prefs)
170        db.appendURL(prefs.pimpDatabase)
171        self.setDB(dbUrl, db)
172
173    def checkUpdates_(self, sender):
174        """
175        Refresh the package information, the user may have installed or
176        removed a package. This method is called once in a while using a timer.
177        """
178        if self.packageTable is None: return
179
180        self.sortPackages()
181        self.packageTable.reloadData()
182
183    def windowNibName(self):
184        """ Return the name of the document NIB """
185        return 'PackageDatabase'
186
187    def displayName(self):
188        """ Return the document name for inside the window title """
189        if self.pimp is None:
190            return "Untitled"
191
192        return DB_URL(self.pimp)
193
194    def awakeFromNib(self):
195        """
196        Initialize the GUI now that the NIB has been loaded.
197        """
198        if self.pimp is not None:
199            self.databaseName.setStringValue_(DB_DESCRIPTION(self.pimp))
200            self.databaseMaintainer.setStringValue_(DB_MAINTAINER(self.pimp))
201
202        else:
203            self.databaseName.setStringValue_("")
204            self.databaseMaintainer.setStringValue_("")
205
206
207        self.setBoolFromDefaults(self.verbose, 'verbose')
208        self.setBoolFromDefaults(
209                self.installDependencies, 'installDependencies')
210        self.setBoolFromDefaults(self.showHidden, 'showHidden')
211        self.setBoolFromDefaults(self.overwrite, 'forceInstallation')
212
213        b = NSUserDefaults.standardUserDefaults(
214                ).boolForKey_('installSystemWide')
215        if b:
216            self.installationLocation.setState_atRow_column_(NSOnState, 0, 0)
217        else:
218            self.installationLocation.setState_atRow_column_(NSOnState, 1, 0)
219
220        self.sortPackages()
221
222    def setBoolFromDefaults(self, field, name):
223        defaults = NSUserDefaults.standardUserDefaults()
224        b = defaults.boolForKey_(name)
225        if b:
226            field.setState_(NSOnState)
227        else:
228            field.setState_(NSOffState)
229
230    def saveBoolToDefaults(self, field, name):
231        defaults = NSUserDefaults.standardUserDefaults()
232        defaults.setBool_forKey_(field.state() == NSOnState, name)
233        defaults.synchronize()
234
235    @objc.IBAction
236    def savePreferences_(self, sender):
237        self.saveBoolToDefaults(self.verbose, 'verbose')
238        self.saveBoolToDefaults(self.installDependencies, 'installDependencies')
239        self.saveBoolToDefaults(self.showHidden, 'showHidden')
240        self.saveBoolToDefaults(self.overwrite, 'forceInstallation')
241        self.saveBoolToDefaults(
242                self.installationLocation.cellAtRow_column_(0, 0),
243                'installSystemWide')
244
245    def packages(self):
246        return self._packages
247
248    def selectedPackage(self):
249        row = self.packageTable.selectedRow()
250        if row == -1: return None
251
252        return self._packages[row]
253
254
255    def tableViewSelectionDidChange_(self, obj):
256        """
257        Update the detail view
258        """
259
260        package = self.selectedPackage()
261
262        if package is None:
263            # No selected package, clear the detail view
264            setString(self.itemHome, None)
265            setString(self.itemStatus, None)
266            setString(self.itemInstalled, None)
267            self.itemDescription.setString_("")
268            self.installButton.setEnabled_(False)
269            self._prerequisites = []
270            self.prerequisitesTable.reloadData()
271
272        else:
273            # Update the detail view
274
275            setString(self.itemHome, package.homepage())
276
277            # XXX: Could we use ReST for the the description?
278            # Recognizing and 'activating' URL's would be fairly easy.
279            self.itemDescription.setString_(
280                    package.description()
281            )
282
283            status, msg = package.installed()
284            setString(self.itemInstalled, status)
285            setString(self.itemStatus, msg)
286            self.installButton.setEnabled_(True)
287            self._prerequisites = package.prerequisites()
288
289            # XXX: Add the closure of all dependencies
290
291            self.prerequisitesTable.reloadData()
292
293    @objc.IBAction
294    def addToFavorites_(self, sender):
295        appdel = NSApplication.sharedApplication().delegate()
296        appdel.addFavorite(self.pimp._description, self.pimp._urllist[0])
297
298    #
299    # NSTableDataSource implementation, for the package list
300    #
301
302    def numberOfRowsInTableView_(self, view):
303
304        if not hasattr(self, 'pimp') or self.pimp is None:
305            return 0
306
307        if view is self.packageTable:
308            return len(self._packages)
309        else:
310            return len(self._prerequisites)
311
312
313    def tableView_objectValueForTableColumn_row_(self, view, col, row):
314
315        colname = col.identifier()
316
317        if view is self.packageTable:
318            package = self._packages[row]
319            shortdescription = None
320        else:
321            package, shortdescription = self._prerequisites[row]
322
323        if colname == 'installed':
324            # XXX: Nicer formatting
325            return getattr(package, colname)()[0]
326
327        return getattr(package, colname)()
328
329    def tableView_sortDescriptorsDidChange_(self, view, oldDescriptors):
330        if view is self.packageTable:
331            self.sortPackages()
332
333    def sortPackages(self):
334        """
335        Sort the package list in the order wished for by the user.
336        """
337        if self.pimp is None:
338            return
339
340        if self.packageTable is None:
341            return
342
343        sortInfo = [
344            (item.key(), item.ascending(), item.selector())
345                for item in self.packageTable.sortDescriptors()
346        ]
347
348        if self.showHidden.state() == NSOnState:
349            self._packages = self.pimp.list()[:]
350        else:
351            self._packages = [ pkg
352                for pkg in self.pimp.list() if not PKG_HIDDEN(pkg) ]
353
354        if not sortInfo:
355            self.packageTable.reloadData()
356            self.tableViewSelectionDidChange_(None)
357            return
358
359        def cmpBySortInfo(l, r):
360            for key, ascending, meth in sortInfo:
361                if key == 'installed':
362                    l_val = getattr(l, key)()[0]
363                    r_val = getattr(r, key)()[0]
364                else:
365                    l_val = getattr(l, key)()
366                    r_val = getattr(r, key)()
367                if meth == 'compare:':
368                    res = cmp(l_val, r_val)
369                else:
370                    if isinstance(l_val, objc.pyobjc_unicode):
371                        l_val = l_val.nsstring()
372                    elif isinstance(l_val, (unicode, str)):
373                        l_val = NSString.stringWithString_(l_val).nsstring()
374                    res = getattr(l_val, meth)(r_val)
375
376                if not ascending:
377                    res = -res
378                if res != 0:
379                    return res
380
381            return 0
382
383        self._packages.sort(cmpBySortInfo)
384        self.packageTable.reloadData()
385
386    @objc.IBAction
387    def filterPackages_(self, sender):
388        """
389        GUI action that is triggered when one of the view options
390        changes
391        """
392        self.sortPackages()
393
394    @objc.IBAction
395    def visitHome_(self, sender):
396        """
397        Open the homepage of the currently selected package in the
398        default webbrowser.
399        """
400        package = self.selectedPackage()
401        if package is None:
402            return
403
404        home = package.homepage()
405        if home is None:
406            return
407
408        try:
409            webbrowser.open(home)
410        except Exception, msg:
411            NSBeginAlertSheet(
412                    'Opening homepage failed',
413                    'OK', None, None, self.windowForSheet(), None, None, None,
414                    0, 'Could not open homepage: %s'%(msg,))
415
416
417    @objc.IBAction
418    def installPackage_(self, sender):
419        """
420        Install the currently selected package
421        """
422        package = self.selectedPackage()
423        if package is None: return
424
425        force = self.overwrite.state() == NSOnState
426        recursive = self.installDependencies.state() == NSOnState
427
428        pimpInstaller = pimp.PimpInstaller(self.pimp)
429        lst, messages = pimpInstaller.prepareInstall(package, force, recursive)
430
431        if messages:
432            NSBeginAlertSheet(
433                    'Cannot install packages',
434                    'OK', None, None,
435                    self.windowForSheet(), None, None, None, 0,
436                    '\n'.join(messages))
437            return
438
439        app = NSApplication.sharedApplication()
440        self.installationTitle.setStringValue_(
441                'Installing: %s ...'%(package.shortdescription(),))
442        self.installationProgress.setHidden_(False)
443        self.installationProgress.startAnimation_(self)
444        self.progressOK.setEnabled_(False)
445        ts = self.installationLog.textStorage()
446        ts.deleteCharactersInRange_((0, ts.length()))
447        app.beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_(
448                self.installationPanel,
449                self.windowForSheet(),
450                None, None, 0)
451
452        # I'm not sure if this accidental or not, but prepareInstall() returns
453        # a list of package in the order that they should be installed in,
454        # and install() installs them in the reverse order :-(
455        # XXX: This seems to be a bug in pimp.
456        self.runner = InstallerThread(
457                            self,
458                            pimpInstaller,
459                            lst[::-1],
460                            self.verbose.state() == NSOnState,
461                            self.installationLog.textStorage()
462                        )
463
464        self.runner.start()
465
466    @objc.IBAction
467    def closeProgress_(self, sender):
468        """
469        Close the installation progress sheet
470        """
471        self.installationPanel.close()
472        NSApplication.sharedApplication().endSheet_(self.installationPanel)
473
474    @objc.IBAction
475    def installationDone_(self, sender):
476        """
477        The installer thread is ready, close the sheet.
478        """
479        self.progressOK.setEnabled_(True)
480        self.installationProgress.setHidden_(False)
481        self.installationProgress.stopAnimation_(self)
482
483        messages = self.runner.result
484        if messages:
485            ts = self.installationLog.textStorage()
486            ts.appendAttributedString_(
487                NSAttributedString.alloc().initWithString_attributes_(
488                    '\n\nCannot install packages\n\n',
489                    {
490                        NSFontAttributeName: NSFont.boldSystemFontOfSize_(12),
491                    }
492                ))
493
494            ts.appendAttributedString_(
495                NSAttributedString.alloc().initWithString_(
496                    '\n'.join(messages) + '\n'))
497
498        self.packageTable.reloadData()
499        self.tableViewSelectionDidChange_(None)
500
501
502class DownloadThread (threading.Thread):
503    """
504    Thread for downloading a PackageManager database.
505
506    This is used by the application delegate to open databases.
507    """
508    daemon_thread = True
509
510    def __init__(self, master, document, url):
511        """
512        Initialize the thread.
513
514        master   - NSObject implementing dbReceived: and dbProblem:
515        document - An PackageDatabase
516        url      - The PackMan URL
517        """
518        threading.Thread.__init__(self)
519        self.master = master
520        self.document = document
521        self.url = url
522
523    def run(self):
524        """
525        Run the thread. This creates a new pimp.PimpDatabase, tells it to
526        download our database and then forwards the database to the
527        master. The last step is done on the main thread because of Cocoa
528        threading issues.
529        """
530        pool = NSAutoreleasePool.alloc().init()
531
532        try:
533            prefs = pimp.PimpPreferences()
534            if self.url is not None:
535                prefs.pimpDatabase = self.url
536            else:
537                prefs.pimpDatabase = pimp.DEFAULT_PIMPDATABASE
538
539            db = pimp.PimpDatabase(prefs)
540            db.appendURL(prefs.pimpDatabase)
541
542            self.master.performSelectorOnMainThread_withObject_waitUntilDone_(
543                'dbReceived:', (self.document, self.url, db), False)
544
545        except:
546            self.master.performSelectorOnMainThread_withObject_waitUntilDone_(
547                'dbProblem:', (self.document, self.url, sys.exc_info()), False)
548
549        del pool
550
551
552
553
554class InstallerThread (threading.Thread):
555    """
556    A thread for installing packages.
557
558    Like downloading a database, installing (and downloading!) packages is
559    a time-consuming task that is better done on a seperate thread.
560    """
561    daemon_thread = True
562
563    def __init__(self, document, installer, packages, verbose, textStorage):
564        threading.Thread.__init__(self)
565        self.document = document
566        self.installer = installer
567        self.packages = packages
568        self.verbose = verbose
569        self.textStorage = textStorage
570        self.result = None
571
572    def write(self, data):
573        self.textStorage.performSelectorOnMainThread_withObject_waitUntilDone_(
574                'appendAttributedString:',
575                NSAttributedString.alloc().initWithString_(data),
576                False)
577
578    def run(self):
579        pool = NSAutoreleasePool.alloc().init()
580
581        if self.verbose:
582            result = self.installer.install(self.packages, self)
583        else:
584            result = self.installer.install(self.packages, None)
585
586        self.write('\nDone.\n')
587
588        self.document.performSelectorOnMainThread_withObject_waitUntilDone_(
589                'installationDone:', None, False)
590
591        del pool
592
593class URLOpener (NSObject):
594    """
595    Model/controller for the 'File/Open URL...' panel
596    """
597    okButton = objc.IBOutlet
598    urlField = objc.IBOutlet()
599
600    def __del__(self):
601        # XXX: I'm doing something wrong, this function is never called!
602        print "del URLOpener %#x"%(id(self),)
603
604
605    def awakeFromNib(self):
606        self.urlField.window().makeKeyAndOrderFront_(None)
607
608    @objc.IBAction
609    def doOpenURL_(self, sender):
610        url = self.urlField.stringValue()
611        if not url:
612            return
613
614        # Ask the application delegate to open the selected database
615        NSApplication.sharedApplication().delegate().openDatabase(url)
616
617    @objc.IBAction
618    def controlTextDidChange_(self, sender):
619        """
620        The value of the URL input field changed, enable the OK button
621        if there is input, disable it otherwise.
622        """
623        if self.urlField.stringValue() != "":
624            self.okButton.setEnabled_(True)
625        else:
626            self.okButton.setEnabled_(False)
627
628
629
630class PackageManager (NSObject):
631    """
632    Application controller: application-level callbacks and actions
633    """
634    favoritesPanel = objc.IBOutlet()
635    favoritesTable = objc.IBOutlet()
636    favoritesTitle = objc.IBOutlet()
637    favoritesURL   = objc.IBOutlet()
638
639    #
640    # Standard actions
641    #
642
643    def awakeFromNib(self):
644        """
645        We've been restored from the NIB
646        """
647        self.loadFavorites()
648
649    #
650    # Working with favorites
651    #
652    # The favorites are stored in the user defaults for the application.
653
654    def loadFavorites(self):
655        """
656        Load our favorite database
657        """
658        self.favorites = NSUserDefaults.standardUserDefaults().arrayForKey_(
659                    'favorites')
660        if self.favorites is None:
661            self.favorites = []
662        else:
663            self.favorites = list(self.favorites)
664
665    def saveFavorites(self):
666        """
667        Save the favorites database, must be called whenever self.favorites
668        is changed.
669        """
670        defaults = NSUserDefaults.standardUserDefaults()
671        defaults.setObject_forKey_(
672                self.favorites,
673                'favorites')
674        defaults.synchronize()
675
676    def addFavorite(self, title, url):
677        """
678        Add a new favorite, and save the database
679        """
680        self.favorites.append({'title':title, 'URL':url})
681        self.favoritesTable.reloadData()
682        self.saveFavorites()
683
684    def menuNeedsUpdate_(self, menu):
685        """
686        We're the delegate for the Favorites menu
687
688        Update the menu: it should list the entries in the favorites database.
689        """
690        menuLen = menu.numberOfItems()
691
692        # Remove old items
693        for i in range(menuLen-1, 2, -1):
694            menu.removeItemAtIndex_(i)
695
696        # Insert new ones
697        for item in self.favorites:
698            title = item['title']
699            url = item['URL']
700
701            mi = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
702                    title, self.openFavorite_, "")
703            mi.setTarget_(self)
704            mi.setRepresentedObject_(item)
705            menu.addItem_(mi)
706
707
708    def tableViewSelectionDidChange_(self, obj):
709        """
710        We're the delegate (and datasource) for the favorites list in the
711        edit pane for the favorites.
712
713        Update the input fields to show the current item.
714        """
715
716        row = self.favoritesTable.selectedRow()
717        if row == -1:
718            self.favoritesTitle.setStringValue_('')
719            self.favoritesURL.setStringValue_('')
720            self.favoritesTitle.setEnabled_(False)
721            self.favoritesURL.setEnabled_(False)
722        else:
723            self.favoritesTitle.setStringValue_(self.favorites[row]['title'])
724            self.favoritesURL.setStringValue_(self.favorites[row]['URL'])
725            self.favoritesTitle.setEnabled_(True)
726            self.favoritesURL.setEnabled_(True)
727
728
729    def numberOfRowsInTableView_(self, view):
730        """
731        We're the datasource for the favorites list in the Favorites panel
732        """
733        if not hasattr(self, 'favorites'):
734            return 0
735
736        return len(self.favorites)
737
738    def tableView_objectValueForTableColumn_row_(self, view, col, row):
739        """
740        We're the datasource for the favorites list in the Favorites panel
741        """
742        return self.favorites[row]['title']
743
744    @objc.IBAction
745    def changeFavoritesTitle_(self, sender):
746        """
747        Update the title of the currently selected favorite item
748        """
749        row = self.favoritesTable.selectedRow()
750        if row == -1:
751            return
752
753        self.favorites[row]['title'] = self.favoritesTitle.stringValue()
754        self.saveFavorites()
755
756        self.favoritesTable.reloadData()
757
758
759    @objc.IBAction
760    def changeFavoritesUrl_(self, sender):
761        """
762        Update the URL of the currently selected favorite item
763        """
764        row = self.favoritesTable.selectedRow()
765        if row == -1:
766            return
767
768        self.favorites[row]['URL'] = self.favoritesURL.stringValue()
769        self.saveFavorites()
770
771        self.favoritesTable.reloadData()
772
773    @objc.IBAction
774    def openFavorite_(self, sender):
775        """
776        Open a favorite database (action for entries in the Favorites menu)
777        """
778        self.openDatabase(sender.representedObject()['URL'])
779
780
781    #
782    # Global actions/callbacks
783    #
784
785    def openDatabase(self, url):
786        """
787        Create a new NSDocument for the database at the specified URL.
788        """
789        doc = NSDocumentController.sharedDocumentController(
790                ).openUntitledDocumentOfType_display_(DB_FILE_TYPE, False)
791        try:
792            downloader = DownloadThread(self, doc, url)
793            downloader.start()
794        except:
795            doc.close()
796            raise
797
798    def dbReceived_(self, (doc, url, db)):
799        doc.setDB(url, db)
800        doc.showWindows()
801
802    def dbProblem_(self, (doc, url, exc_info)):
803        NSRunAlertPanel(
804                "Cannot open database",
805                "Opening database at %s failed: %s"%(url, exc_info[1]),
806                "OK", None, None)
807        doc.close()
808
809
810
811    @objc.IBAction
812    def openURL_(self, sender):
813        """
814        The user wants to open a package URL, show the user-interface.
815        """
816        res = NSBundle.loadNibNamed_owner_('OpenPanel', self)
817
818    @objc.IBAction
819    def openStandardDatabase_(self, sender):
820        """
821        Open the standard database.
822        """
823        self.openDatabase(pimp.DEFAULT_PIMPDATABASE)
824
825    def applicationShouldOpenUntitledFile_(self, app):
826        """
827        The default window is not an untitled window, but the default
828        database
829        """
830        return False
831
832    def applicationDidFinishLaunching_(self, app):
833        """
834        The application finished launching, show the default database.
835        """
836        # XXX: We shouldn't open the standard database if the user explicitly
837        # opened another one!
838        self.openStandardDatabase_(None)
839
840#
841# Set some sensible defaults
842#
843NSUserDefaults.standardUserDefaults().registerDefaults_(
844        {
845          'verbose': True,
846          'installDependencies': True,
847          'showHidden': False,
848          'forceInstallation': False,
849          'installSystemWide': True,
850        })
851
852#
853# A nasty hack. For some reason sys.prefix is /usr/bin/../../System/..., while
854# it is /System/... in Jack's PackageManager.app.  At least one package
855# manager database relies on sys.prefix being /System/... (Bob's additional
856# packages).
857#
858import os
859sys.prefix = os.path.abspath(sys.prefix)
860
861AppHelper.runEventLoop()
862