[Pythonmac-SIG] The where's-my-app problem

Bob Ippolito bob at redivi.com
Thu Nov 18 17:47:52 CET 2004


On Nov 18, 2004, at 4:17 PM, Charles Hartman wrote:

> Over on the wxPython-users list, there has been endless discussion of  
> this problem (see
> http://lists.wxwidgets.org/cgi-bin/ezmlm-cgi?11:sss:33832: 
> fkbngdnmdfjfdjpcdndi#b):
>
>  I want to know the directory from which my app was launched, so as to  
> locate a couple of data files that need to be editable and therefore  
> separate from the bundle, painlessly and without dialog boxes in the  
> normal case (the files haven't been moved, the app is started by  
> double-clicking or whatever is most obvious to a casual user). It  
> turns out to be strangely difficult to do this in a way that works for  
> (a) apps launched from the Terminal and (b) apps frozen by py2app on  
> Mac and (c) py2exe apps on Windows.
>
> In several of the approaches proposed, the py2app "frozen" app seems  
> to be the odd one out. IF I've got this straight, these are the two  
> that work on Win2K running from Python and in a py2exe app, and on  
> OS/X running from Python, but not from a bundlebuilder (or, I think,  
> py2app) app:

<the given methods are either totally unreliable or incomplete at best>

I'm just going to spew a short article's worth of information about  
this topic.  A subset of this will certainly answer your question.   
Feel free to reproduce this in any way you see fit (i.e. a FAQ or Wiki  
would be a good place for this information), but be warned that this  
code is written from Mail.app and is untested, so it may have typos.


== Mac OS X and paths ==

 From a Mac OS X application's perspective, the "directory from which  
your app was launched" is completely ambiguous.  Is is so ambiguous, in  
fact, that when a Mac OS X application starts up by way of  
LaunchServices (i.e. Finder, /usr/bin/open, etc.) it will have a  
current directory of /.  Yes, that's right.  It starts in the root of  
your filesystem, a completely useless place.  The reason for this is  
that on Mac OS X, you should always always always always ALWAYS ALWAYS  
find files in one of the following ways:

- Inside your application bundle (via CFBundle or NSBundle functions)
- A predefined system path (via FSFindFolder or  
NSSearchPathForDirectoriesInDomains, NOT by hardcoding something) or a  
subdirectory of one (i.e. ~/Library/Application Support/MyApplication)
- Chosen by the user with some kind of system dialog
- A "remembered" (using an alias) location that was chosen by the user  
with some kind of dialog at one point another (system dialogs do this,  
so that they remember where the user last chose files from)

That's it.  You should ALMOST NEVER use any other way to find stuff.

ALL data files of a correctly constructed Mac OS X application should  
be in the Resources folder of the bundle.  If you want to have user  
editable versions of these files, then on startup you should make  
copies of the in-Resources files somewhere like ~/Library/Application  
Support/MyApplication if they do not exist and use those.  You may also  
add functionality to your application to reveal this path to the user.   
For example, the Scripts menu in Mail.app has an "Open Scripts Folder"  
menu.


== Canonical way of detecting py2app, py2exe ==

def find_packager():
     import sys
     frozen = getattr(sys, 'frozen', None)
     if not frozen:
         # COULD be certain cx_Freeze options or bundlebuilder, nothing  
to worry about though
         return None
     elif frozen in ('dll', 'console_exe', 'windows_exe'):
         return 'py2exe'
     elif frozen in ('macosx_app',):
         return 'py2app'
     elif frozen is True:
         # it doesn't ALWAYS set this
         return 'cx_Freeze'
     else:
         return '<unknown packager: %r>' % (frozen,)

It's possible to build cx_Freeze-frozen applications that are not  
picked up by this (i.e. ConsoleKeepPath).  Bundlebuilder is never  
really detectable, but you should NEVER use bundlebuilder.  py2app is  
capable of performing all of the operations that bundlebuilder can do  
(and then some), with the added bonus that its implementation is more  
correct.  I hope to deprecate bundlebuilder in Python 2.5.


== Canonical way of finding your main script ==

import os
def mainScriptDir():
     import __main__
     return os.path.dirname(os.path.abspath(__main__.__file__))

Finding your main script should be done in the same way that your main  
script determines if it is main or not!  For applications frozen with  
py2exe, there IS effectively no on-disk main script (the bytecode is  
shoved into the executable).  Finding your main script is not a useful  
thing to do from a packaged application anyway, so only use this when  
find_packager() returns None.


== Default invariants for py2app ==

- sys.frozen == 'macosx_app'
- os.getcwd() is the Resources directory of your application's bundle  
when the application starts.  This is a convenience to make most  
scripts work "out of the box" (but probably after using the  
argv_emulation option).
- os.environ['RESOURCEPATH'] is the absolute path to the Resources  
directory of your application's bundle when it started
- The user's "site-packages" directory WILL NOT be considered by  
default, so you
- Any Python code, dynamic libraries, and frameworks that are  
determined to be used, aren't explicitly excluded, and aren't located  
in a system path (/usr/lib or /System) WILL end up somewhere accessible  
from the runtime in your application bundle.  You SHOULD ONLY depend on  
the fact that the code can be located at runtime, NOT where it is in  
the application bundle.
- NO non-code will be included from packages UNLESS the package is  
explicitly included using the "packages" option.  This can also happen  
implicitly by way of a recipe.
- Such explicitly included packages are included as-is, but may be  
stripped of 'CVS' and '.svn' subdirectories.  You CAN depend on data  
files that are in such packages, but you should ONLY reference them  
relative to the dirname of the package's __file__.


== Don't use bundlebuilder ==

bundlebuilder does a number of things incorrectly and incompletely.   
Don't use it anymore, ever.  A typical py2app setup script is quite a  
bit simpler than the bundlebuilder version, anyway.


== Using Python packages as data file containers ==

It's common to include necessary data files inside of packages, for  
example:

mypackage/
     __init__.py
     mydatafile

To locate such data files, you should always do something equivalent to  
the following:

import mypackage
mydatafilepath =  
os.path.join(os.path.dirname(os.path.abspath(mypackage.__file__)),  
'mydatafile')

This method will not work in the face of zip imports, so such packages  
must be explicitly included as-is with packaging solutions such as  
py2app or py2exe.


== If you *actually* want to find something relative to the  
"application" ==

Note that this code only belongs in a poorly designed OS X application,  
but I'm going to tell you anyway since this seems to be what you asked  
for after a painful reading of the referenced wxPython-users thread:

# use find_packager() above
import os
def getApplicationPath():
     packager = find_packager()
     if packager == 'py2exe':
         # note that another approach is
         return os.path.dirname(os.path.abspath(sys.argv[0]))
     elif packager == 'py2app':
         # from path/Foo.app/Contents/Resources -> path, even if  
something in your application chdir'ed in the meantime
         return  
os.path.dirname(os.path.dirname(os.path.dirname(os.environ['RESOURCEPATH 
'])))
     else:
         import __main__
         return os.path.dirname(__main__.__file__)

Also note that this isn't bulletproof.

For example, in one of my applications, I use NSIS on top of py2exe to  
provide a "single file executable" (for which the above code would  
return a nonsense temporary directory).  In the Mac OS X version of  
this application all of my resources are located in the Resources  
folder of the application bundle, and I locate them using OS X specific  
APIs (NSBundle, etc.) since the application is written with PyObjC.  In  
the Win32 version, NSIS sets the current directory to the location of  
the "single file executable", and the windows version searches that  
directory for the Mac OS X application, and uses the resources folder  
of its application bundle for data files.  I emulate many of the  
localization and file-finding capabilities of Cocoa in the Windows  
code.  This is of course a strange way to do things on Windows, since  
the canonical way is to use an installer, but in this case the  
application *is* an installer (to put data on a peripheral device, not  
more software for the machine) and is rarely executed, so it makes  
perfect sense to do it in this manner.

Hopefully this covers everything you need.  Feel free to ask additional  
questions, but I'm pretty busy with the pypy sprint and I'm in a weird  
timezone (gmt+2) for the next week, so it may take a while to respond.

-bob



More information about the Pythonmac-SIG mailing list