Home | History | Annotate | Download | only in plat-mac
      1 """tools for BuildApplet and BuildApplication"""
      2 
      3 import warnings
      4 warnings.warnpy3k("the buildtools module is deprecated and is removed in 3.0",
      5               stacklevel=2)
      6 
      7 import sys
      8 import os
      9 import string
     10 import imp
     11 import marshal
     12 from Carbon import Res
     13 import Carbon.Files
     14 import Carbon.File
     15 import MacOS
     16 import macostools
     17 import macresource
     18 try:
     19     import EasyDialogs
     20 except ImportError:
     21     EasyDialogs = None
     22 import shutil
     23 
     24 
     25 BuildError = "BuildError"
     26 
     27 # .pyc file (and 'PYC ' resource magic number)
     28 MAGIC = imp.get_magic()
     29 
     30 # Template file (searched on sys.path)
     31 TEMPLATE = "PythonInterpreter"
     32 
     33 # Specification of our resource
     34 RESTYPE = 'PYC '
     35 RESNAME = '__main__'
     36 
     37 # A resource with this name sets the "owner" (creator) of the destination
     38 # It should also have ID=0. Either of these alone is not enough.
     39 OWNERNAME = "owner resource"
     40 
     41 # Default applet creator code
     42 DEFAULT_APPLET_CREATOR="Pyta"
     43 
     44 # OpenResFile mode parameters
     45 READ = 1
     46 WRITE = 2
     47 
     48 # Parameter for FSOpenResourceFile
     49 RESOURCE_FORK_NAME=Carbon.File.FSGetResourceForkName()
     50 
     51 def findtemplate(template=None):
     52     """Locate the applet template along sys.path"""
     53     if MacOS.runtimemodel == 'macho':
     54         return None
     55     if not template:
     56         template=TEMPLATE
     57     for p in sys.path:
     58         file = os.path.join(p, template)
     59         try:
     60             file, d1, d2 = Carbon.File.FSResolveAliasFile(file, 1)
     61             break
     62         except (Carbon.File.Error, ValueError):
     63             continue
     64     else:
     65         raise BuildError, "Template %r not found on sys.path" % (template,)
     66     file = file.as_pathname()
     67     return file
     68 
     69 def process(template, filename, destname, copy_codefragment=0,
     70         rsrcname=None, others=[], raw=0, progress="default", destroot=""):
     71 
     72     if progress == "default":
     73         if EasyDialogs is None:
     74             print "Compiling %s"%(os.path.split(filename)[1],)
     75             process = None
     76         else:
     77             progress = EasyDialogs.ProgressBar("Processing %s..."%os.path.split(filename)[1], 120)
     78             progress.label("Compiling...")
     79             progress.inc(0)
     80     # check for the script name being longer than 32 chars. This may trigger a bug
     81     # on OSX that can destroy your sourcefile.
     82     if '#' in os.path.split(filename)[1]:
     83         raise BuildError, "BuildApplet could destroy your sourcefile on OSX, please rename: %s" % filename
     84     # Read the source and compile it
     85     # (there's no point overwriting the destination if it has a syntax error)
     86 
     87     fp = open(filename, 'rU')
     88     text = fp.read()
     89     fp.close()
     90     try:
     91         code = compile(text + '\n', filename, "exec")
     92     except SyntaxError, arg:
     93         raise BuildError, "Syntax error in script %s: %s" % (filename, arg)
     94     except EOFError:
     95         raise BuildError, "End-of-file in script %s" % (filename,)
     96 
     97     # Set the destination file name. Note that basename
     98     # does contain the whole filepath, only a .py is stripped.
     99 
    100     if string.lower(filename[-3:]) == ".py":
    101         basename = filename[:-3]
    102         if MacOS.runtimemodel != 'macho' and not destname:
    103             destname = basename
    104     else:
    105         basename = filename
    106 
    107     if not destname:
    108         if MacOS.runtimemodel == 'macho':
    109             destname = basename + '.app'
    110         else:
    111             destname = basename + '.applet'
    112     if not rsrcname:
    113         rsrcname = basename + '.rsrc'
    114 
    115     # Try removing the output file. This fails in MachO, but it should
    116     # do any harm.
    117     try:
    118         os.remove(destname)
    119     except os.error:
    120         pass
    121     process_common(template, progress, code, rsrcname, destname, 0,
    122         copy_codefragment, raw, others, filename, destroot)
    123 
    124 
    125 def update(template, filename, output):
    126     if MacOS.runtimemodel == 'macho':
    127         raise BuildError, "No updating yet for MachO applets"
    128     if progress:
    129         if EasyDialogs is None:
    130             print "Updating %s"%(os.path.split(filename)[1],)
    131             progress = None
    132         else:
    133             progress = EasyDialogs.ProgressBar("Updating %s..."%os.path.split(filename)[1], 120)
    134     else:
    135         progress = None
    136     if not output:
    137         output = filename + ' (updated)'
    138 
    139     # Try removing the output file
    140     try:
    141         os.remove(output)
    142     except os.error:
    143         pass
    144     process_common(template, progress, None, filename, output, 1, 1)
    145 
    146 
    147 def process_common(template, progress, code, rsrcname, destname, is_update,
    148         copy_codefragment, raw=0, others=[], filename=None, destroot=""):
    149     if MacOS.runtimemodel == 'macho':
    150         return process_common_macho(template, progress, code, rsrcname, destname,
    151             is_update, raw, others, filename, destroot)
    152     if others:
    153         raise BuildError, "Extra files only allowed for MachoPython applets"
    154     # Create FSSpecs for the various files
    155     template_fsr, d1, d2 = Carbon.File.FSResolveAliasFile(template, 1)
    156     template = template_fsr.as_pathname()
    157 
    158     # Copy data (not resources, yet) from the template
    159     if progress:
    160         progress.label("Copy data fork...")
    161         progress.set(10)
    162 
    163     if copy_codefragment:
    164         tmpl = open(template, "rb")
    165         dest = open(destname, "wb")
    166         data = tmpl.read()
    167         if data:
    168             dest.write(data)
    169         dest.close()
    170         tmpl.close()
    171         del dest
    172         del tmpl
    173 
    174     # Open the output resource fork
    175 
    176     if progress:
    177         progress.label("Copy resources...")
    178         progress.set(20)
    179     try:
    180         output = Res.FSOpenResourceFile(destname, RESOURCE_FORK_NAME, WRITE)
    181     except MacOS.Error:
    182         destdir, destfile = os.path.split(destname)
    183         Res.FSCreateResourceFile(destdir, unicode(destfile), RESOURCE_FORK_NAME)
    184         output = Res.FSOpenResourceFile(destname, RESOURCE_FORK_NAME, WRITE)
    185 
    186     # Copy the resources from the target specific resource template, if any
    187     typesfound, ownertype = [], None
    188     try:
    189         input = Res.FSOpenResourceFile(rsrcname, RESOURCE_FORK_NAME, READ)
    190     except (MacOS.Error, ValueError):
    191         pass
    192         if progress:
    193             progress.inc(50)
    194     else:
    195         if is_update:
    196             skip_oldfile = ['cfrg']
    197         else:
    198             skip_oldfile = []
    199         typesfound, ownertype = copyres(input, output, skip_oldfile, 0, progress)
    200         Res.CloseResFile(input)
    201 
    202     # Check which resource-types we should not copy from the template
    203     skiptypes = []
    204     if 'vers' in typesfound: skiptypes.append('vers')
    205     if 'SIZE' in typesfound: skiptypes.append('SIZE')
    206     if 'BNDL' in typesfound: skiptypes = skiptypes + ['BNDL', 'FREF', 'icl4',
    207             'icl8', 'ics4', 'ics8', 'ICN#', 'ics#']
    208     if not copy_codefragment:
    209         skiptypes.append('cfrg')
    210 ##  skipowner = (ownertype != None)
    211 
    212     # Copy the resources from the template
    213 
    214     input = Res.FSOpenResourceFile(template, RESOURCE_FORK_NAME, READ)
    215     dummy, tmplowner = copyres(input, output, skiptypes, 1, progress)
    216 
    217     Res.CloseResFile(input)
    218 ##  if ownertype is None:
    219 ##      raise BuildError, "No owner resource found in either resource file or template"
    220     # Make sure we're manipulating the output resource file now
    221 
    222     Res.UseResFile(output)
    223 
    224     if ownertype is None:
    225         # No owner resource in the template. We have skipped the
    226         # Python owner resource, so we have to add our own. The relevant
    227         # bundle stuff is already included in the interpret/applet template.
    228         newres = Res.Resource('\0')
    229         newres.AddResource(DEFAULT_APPLET_CREATOR, 0, "Owner resource")
    230         ownertype = DEFAULT_APPLET_CREATOR
    231 
    232     if code:
    233         # Delete any existing 'PYC ' resource named __main__
    234 
    235         try:
    236             res = Res.Get1NamedResource(RESTYPE, RESNAME)
    237             res.RemoveResource()
    238         except Res.Error:
    239             pass
    240 
    241         # Create the raw data for the resource from the code object
    242         if progress:
    243             progress.label("Write PYC resource...")
    244             progress.set(120)
    245 
    246         data = marshal.dumps(code)
    247         del code
    248         data = (MAGIC + '\0\0\0\0') + data
    249 
    250         # Create the resource and write it
    251 
    252         id = 0
    253         while id < 128:
    254             id = Res.Unique1ID(RESTYPE)
    255         res = Res.Resource(data)
    256         res.AddResource(RESTYPE, id, RESNAME)
    257         attrs = res.GetResAttrs()
    258         attrs = attrs | 0x04    # set preload
    259         res.SetResAttrs(attrs)
    260         res.WriteResource()
    261         res.ReleaseResource()
    262 
    263     # Close the output file
    264 
    265     Res.CloseResFile(output)
    266 
    267     # Now set the creator, type and bundle bit of the destination.
    268     # Done with FSSpec's, FSRef FInfo isn't good enough yet (2.3a1+)
    269     dest_fss = Carbon.File.FSSpec(destname)
    270     dest_finfo = dest_fss.FSpGetFInfo()
    271     dest_finfo.Creator = ownertype
    272     dest_finfo.Type = 'APPL'
    273     dest_finfo.Flags = dest_finfo.Flags | Carbon.Files.kHasBundle | Carbon.Files.kIsShared
    274     dest_finfo.Flags = dest_finfo.Flags & ~Carbon.Files.kHasBeenInited
    275     dest_fss.FSpSetFInfo(dest_finfo)
    276 
    277     macostools.touched(destname)
    278     if progress:
    279         progress.label("Done.")
    280         progress.inc(0)
    281 
    282 def process_common_macho(template, progress, code, rsrcname, destname, is_update,
    283         raw=0, others=[], filename=None, destroot=""):
    284     # Check that we have a filename
    285     if filename is None:
    286         raise BuildError, "Need source filename on MacOSX"
    287     # First make sure the name ends in ".app"
    288     if destname[-4:] != '.app':
    289         destname = destname + '.app'
    290     # Now deduce the short name
    291     destdir, shortname = os.path.split(destname)
    292     if shortname[-4:] == '.app':
    293         # Strip the .app suffix
    294         shortname = shortname[:-4]
    295     # And deduce the .plist and .icns names
    296     plistname = None
    297     icnsname = None
    298     if rsrcname and rsrcname[-5:] == '.rsrc':
    299         tmp = rsrcname[:-5]
    300         plistname = tmp + '.plist'
    301         if os.path.exists(plistname):
    302             icnsname = tmp + '.icns'
    303             if not os.path.exists(icnsname):
    304                 icnsname = None
    305         else:
    306             plistname = None
    307     if not icnsname:
    308         dft_icnsname = os.path.join(sys.prefix, 'Resources/Python.app/Contents/Resources/PythonApplet.icns')
    309         if os.path.exists(dft_icnsname):
    310             icnsname = dft_icnsname
    311     if not os.path.exists(rsrcname):
    312         rsrcname = None
    313     if progress:
    314         progress.label('Creating bundle...')
    315     import bundlebuilder
    316     builder = bundlebuilder.AppBuilder(verbosity=0)
    317     builder.mainprogram = filename
    318     builder.builddir = destdir
    319     builder.name = shortname
    320     builder.destroot = destroot
    321     if rsrcname:
    322         realrsrcname = macresource.resource_pathname(rsrcname)
    323         builder.files.append((realrsrcname,
    324             os.path.join('Contents/Resources', os.path.basename(rsrcname))))
    325     for o in others:
    326         if type(o) == str:
    327             builder.resources.append(o)
    328         else:
    329             builder.files.append(o)
    330     if plistname:
    331         import plistlib
    332         builder.plist = plistlib.Plist.fromFile(plistname)
    333     if icnsname:
    334         builder.iconfile = icnsname
    335     if not raw:
    336         builder.argv_emulation = 1
    337     builder.setup()
    338     builder.build()
    339     if progress:
    340         progress.label('Done.')
    341         progress.inc(0)
    342 
    343 ##  macostools.touched(dest_fss)
    344 
    345 # Copy resources between two resource file descriptors.
    346 # skip a resource named '__main__' or (if skipowner is set) with ID zero.
    347 # Also skip resources with a type listed in skiptypes.
    348 #
    349 def copyres(input, output, skiptypes, skipowner, progress=None):
    350     ctor = None
    351     alltypes = []
    352     Res.UseResFile(input)
    353     ntypes = Res.Count1Types()
    354     progress_type_inc = 50/ntypes
    355     for itype in range(1, 1+ntypes):
    356         type = Res.Get1IndType(itype)
    357         if type in skiptypes:
    358             continue
    359         alltypes.append(type)
    360         nresources = Res.Count1Resources(type)
    361         progress_cur_inc = progress_type_inc/nresources
    362         for ires in range(1, 1+nresources):
    363             res = Res.Get1IndResource(type, ires)
    364             id, type, name = res.GetResInfo()
    365             lcname = string.lower(name)
    366 
    367             if lcname == OWNERNAME and id == 0:
    368                 if skipowner:
    369                     continue # Skip this one
    370                 else:
    371                     ctor = type
    372             size = res.size
    373             attrs = res.GetResAttrs()
    374             if progress:
    375                 progress.label("Copy %s %d %s"%(type, id, name))
    376                 progress.inc(progress_cur_inc)
    377             res.LoadResource()
    378             res.DetachResource()
    379             Res.UseResFile(output)
    380             try:
    381                 res2 = Res.Get1Resource(type, id)
    382             except MacOS.Error:
    383                 res2 = None
    384             if res2:
    385                 if progress:
    386                     progress.label("Overwrite %s %d %s"%(type, id, name))
    387                     progress.inc(0)
    388                 res2.RemoveResource()
    389             res.AddResource(type, id, name)
    390             res.WriteResource()
    391             attrs = attrs | res.GetResAttrs()
    392             res.SetResAttrs(attrs)
    393             Res.UseResFile(input)
    394     return alltypes, ctor
    395 
    396 def copyapptree(srctree, dsttree, exceptlist=[], progress=None):
    397     names = []
    398     if os.path.exists(dsttree):
    399         shutil.rmtree(dsttree)
    400     os.mkdir(dsttree)
    401     todo = os.listdir(srctree)
    402     while todo:
    403         this, todo = todo[0], todo[1:]
    404         if this in exceptlist:
    405             continue
    406         thispath = os.path.join(srctree, this)
    407         if os.path.isdir(thispath):
    408             thiscontent = os.listdir(thispath)
    409             for t in thiscontent:
    410                 todo.append(os.path.join(this, t))
    411         names.append(this)
    412     for this in names:
    413         srcpath = os.path.join(srctree, this)
    414         dstpath = os.path.join(dsttree, this)
    415         if os.path.isdir(srcpath):
    416             os.mkdir(dstpath)
    417         elif os.path.islink(srcpath):
    418             endpoint = os.readlink(srcpath)
    419             os.symlink(endpoint, dstpath)
    420         else:
    421             if progress:
    422                 progress.label('Copy '+this)
    423                 progress.inc(0)
    424             shutil.copy2(srcpath, dstpath)
    425 
    426 def writepycfile(codeobject, cfile):
    427     import marshal
    428     fc = open(cfile, 'wb')
    429     fc.write('\0\0\0\0') # MAGIC placeholder, written later
    430     fc.write('\0\0\0\0') # Timestap placeholder, not needed
    431     marshal.dump(codeobject, fc)
    432     fc.flush()
    433     fc.seek(0, 0)
    434     fc.write(MAGIC)
    435     fc.close()
    436