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