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