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