]> git.saurik.com Git - wxWidgets.git/blob - wxPython/distrib/mac/buildpkg.py
wxGridBahSizer support
[wxWidgets.git] / wxPython / distrib / mac / buildpkg.py
1 #!/usr/bin/env python
2
3 """buildpkg.py -- Build OS X packages for Apple's Installer.app.
4
5 This is an experimental command-line tool for building packages to be
6 installed with the Mac OS X Installer.app application.
7
8 It is much inspired by Apple's GUI tool called PackageMaker.app, that
9 seems to be part of the OS X developer tools installed in the folder
10 /Developer/Applications. But apparently there are other free tools to
11 do the same thing which are also named PackageMaker like Brian Hill's
12 one:
13
14 http://personalpages.tds.net/~brian_hill/packagemaker.html
15
16 Beware of the multi-package features of Installer.app (which are not
17 yet supported here) that can potentially screw-up your installation
18 and are discussed in these articles on Stepwise:
19
20 http://www.stepwise.com/Articles/Technical/Packages/InstallerWoes.html
21 http://www.stepwise.com/Articles/Technical/Packages/InstallerOnX.html
22
23 Beside using the PackageMaker class directly, by importing it inside
24 another module, say, there are additional ways of using this module:
25 the top-level buildPackage() function provides a shortcut to the same
26 feature and is also called when using this module from the command-
27 line.
28
29 ****************************************************************
30 NOTE: For now you should be able to run this even on a non-OS X
31 system and get something similar to a package, but without
32 the real archive (needs pax) and bom files (needs mkbom)
33 inside! This is only for providing a chance for testing to
34 folks without OS X.
35 ****************************************************************
36
37 TODO:
38 - test pre-process and post-process scripts (Python ones?)
39 - handle multi-volume packages (?)
40 - integrate into distutils (?)
41
42 Dinu C. Gherman,
43 gherman@europemail.com
44 November 2001
45
46 !! USE AT YOUR OWN RISK !!
47 """
48
49 __version__ = 0.2
50 __license__ = "FreeBSD"
51
52
53 import os, sys, glob, fnmatch, shutil, string, copy, getopt
54 from os.path import basename, dirname, join, islink, isdir, isfile
55
56 Error = "buildpkg.Error"
57
58 PKG_INFO_FIELDS = """\
59 Title
60 Version
61 Description
62 DefaultLocation
63 Diskname
64 DeleteWarning
65 NeedsAuthorization
66 DisableStop
67 UseUserMask
68 Application
69 Relocatable
70 Required
71 InstallOnly
72 RequiresReboot
73 RootVolumeOnly
74 InstallFat\
75 """
76
77 ######################################################################
78 # Helpers
79 ######################################################################
80
81 # Convenience class, as suggested by /F.
82
83 class GlobDirectoryWalker:
84 "A forward iterator that traverses files in a directory tree."
85
86 def __init__(self, directory, pattern="*"):
87 self.stack = [directory]
88 self.pattern = pattern
89 self.files = []
90 self.index = 0
91
92
93 def __getitem__(self, index):
94 while 1:
95 try:
96 file = self.files[self.index]
97 self.index = self.index + 1
98 except IndexError:
99 # pop next directory from stack
100 self.directory = self.stack.pop()
101 self.files = os.listdir(self.directory)
102 self.index = 0
103 else:
104 # got a filename
105 fullname = join(self.directory, file)
106 if isdir(fullname) and not islink(fullname):
107 self.stack.append(fullname)
108 if fnmatch.fnmatch(file, self.pattern):
109 return fullname
110
111
112 ######################################################################
113 # The real thing
114 ######################################################################
115
116 class PackageMaker:
117 """A class to generate packages for Mac OS X.
118
119 This is intended to create OS X packages (with extension .pkg)
120 containing archives of arbitrary files that the Installer.app
121 will be able to handle.
122
123 As of now, PackageMaker instances need to be created with the
124 title, version and description of the package to be built.
125 The package is built after calling the instance method
126 build(root, **options). It has the same name as the constructor's
127 title argument plus a '.pkg' extension and is located in the same
128 parent folder that contains the root folder.
129
130 E.g. this will create a package folder /my/space/distutils.pkg/:
131
132 pm = PackageMaker("distutils", "1.0.2", "Python distutils.")
133 pm.build("/my/space/distutils")
134 """
135
136 packageInfoDefaults = {
137 'Title': None,
138 'Version': None,
139 'Description': '',
140 'DefaultLocation': '/',
141 'Diskname': '(null)',
142 'DeleteWarning': '',
143 'NeedsAuthorization': 'NO',
144 'DisableStop': 'NO',
145 'UseUserMask': 'YES',
146 'Application': 'NO',
147 'Relocatable': 'YES',
148 'Required': 'NO',
149 'InstallOnly': 'NO',
150 'RequiresReboot': 'NO',
151 'RootVolumeOnly' : 'NO',
152 'InstallFat': 'NO'}
153
154
155 def __init__(self, title, version, desc):
156 "Init. with mandatory title/version/description arguments."
157
158 info = {"Title": title, "Version": version, "Description": desc}
159 self.packageInfo = copy.deepcopy(self.packageInfoDefaults)
160 self.packageInfo.update(info)
161
162 # variables set later
163 self.packageRootFolder = None
164 self.packageResourceFolder = None
165 self.sourceFolder = None
166 self.resourceFolder = None
167
168
169 def build(self, root, resources=None, **options):
170 """Create a package for some given root folder.
171
172 With no 'resources' argument set it is assumed to be the same
173 as the root directory. Option items replace the default ones
174 in the package info.
175 """
176
177 # set folder attributes
178 self.sourceFolder = root
179 if resources == None:
180 self.resourceFolder = root
181 else:
182 self.resourceFolder = resources
183
184 # replace default option settings with user ones if provided
185 fields = self. packageInfoDefaults.keys()
186 for k, v in options.items():
187 if k in fields:
188 self.packageInfo[k] = v
189 elif not k in ["OutputDir"]:
190 raise Error, "Unknown package option: %s" % k
191
192 # Check where we should leave the output. Default is current directory
193 outputdir = options.get("OutputDir", os.getcwd())
194 packageName = self.packageInfo["Title"]
195 self.PackageRootFolder = os.path.join(outputdir, packageName + ".pkg")
196
197 # do what needs to be done
198 self._makeFolders()
199 self._addInfo()
200 self._addBom()
201 self._addArchive()
202 self._addResources()
203 self._addSizes()
204
205
206 def _makeFolders(self):
207 "Create package folder structure."
208
209 # Not sure if the package name should contain the version or not...
210 # packageName = "%s-%s" % (self.packageInfo["Title"],
211 # self.packageInfo["Version"]) # ??
212
213 contFolder = join(self.PackageRootFolder, "Contents")
214 self.packageResourceFolder = join(contFolder, "Resources")
215 os.mkdir(self.PackageRootFolder)
216 os.mkdir(contFolder)
217 os.mkdir(self.packageResourceFolder)
218
219 def _addInfo(self):
220 "Write .info file containing installing options."
221
222 # Not sure if options in PKG_INFO_FIELDS are complete...
223
224 info = ""
225 for f in string.split(PKG_INFO_FIELDS, "\n"):
226 info = info + "%s %%(%s)s\n" % (f, f)
227 info = info % self.packageInfo
228 base = self.packageInfo["Title"] + ".info"
229 path = join(self.packageResourceFolder, base)
230 f = open(path, "w")
231 f.write(info)
232
233
234 def _addBom(self):
235 "Write .bom file containing 'Bill of Materials'."
236
237 # Currently ignores if the 'mkbom' tool is not available.
238
239 try:
240 base = self.packageInfo["Title"] + ".bom"
241 bomPath = join(self.packageResourceFolder, base)
242 cmd = "mkbom %s %s" % (self.sourceFolder, bomPath)
243 res = os.system(cmd)
244 except:
245 pass
246
247
248 def _addArchive(self):
249 "Write .pax.gz file, a compressed archive using pax/gzip."
250
251 # Currently ignores if the 'pax' tool is not available.
252
253 cwd = os.getcwd()
254
255 # create archive
256 os.chdir(self.sourceFolder)
257 base = basename(self.packageInfo["Title"]) + ".pax"
258 self.archPath = join(self.packageResourceFolder, base)
259 cmd = "pax -w -f %s %s" % (self.archPath, ".")
260 res = os.system(cmd)
261
262 # compress archive
263 cmd = "gzip %s" % self.archPath
264 res = os.system(cmd)
265 os.chdir(cwd)
266
267
268 def _addResources(self):
269 "Add Welcome/ReadMe/License files, .lproj folders and scripts."
270
271 # Currently we just copy everything that matches the allowed
272 # filenames. So, it's left to Installer.app to deal with the
273 # same file available in multiple formats...
274
275 if not self.resourceFolder:
276 return
277
278 # find candidate resource files (txt html rtf rtfd/ or lproj/)
279 allFiles = []
280 for pat in string.split("*.txt *.html *.rtf *.rtfd *.lproj", " "):
281 pattern = join(self.resourceFolder, pat)
282 allFiles = allFiles + glob.glob(pattern)
283
284 # find pre-process and post-process scripts
285 # naming convention: packageName.{pre,post}_{upgrade,install}
286 # Alternatively the filenames can be {pre,post}_{upgrade,install}
287 # in which case we prepend the package name
288 packageName = self.packageInfo["Title"]
289 for pat in ("*upgrade", "*install", "*flight"):
290 pattern = join(self.resourceFolder, packageName + pat)
291 pattern2 = join(self.resourceFolder, pat)
292 allFiles = allFiles + glob.glob(pattern)
293 allFiles = allFiles + glob.glob(pattern2)
294
295 # check name patterns
296 files = []
297 for f in allFiles:
298 for s in ("Welcome", "License", "ReadMe"):
299 if string.find(basename(f), s) == 0:
300 files.append((f, f))
301 if f[-6:] == ".lproj":
302 files.append((f, f))
303 elif basename(f) in ["pre_upgrade", "pre_install", "post_upgrade", "post_install"]:
304 files.append((f, packageName+"."+basename(f)))
305 elif basename(f) in ["preflight", "postflight"]:
306 files.append((f, f))
307 elif f[-8:] == "_upgrade":
308 files.append((f,f))
309 elif f[-8:] == "_install":
310 files.append((f,f))
311
312 # copy files
313 for src, dst in files:
314 src = basename(src)
315 dst = basename(dst)
316 f = join(self.resourceFolder, src)
317 if isfile(f):
318 shutil.copy(f, os.path.join(self.packageResourceFolder, dst))
319 elif isdir(f):
320 # special case for .rtfd and .lproj folders...
321 d = join(self.packageResourceFolder, dst)
322 os.mkdir(d)
323 files = GlobDirectoryWalker(f)
324 for file in files:
325 shutil.copy(file, d)
326
327
328 def _addSizes(self):
329 "Write .sizes file with info about number and size of files."
330
331 # Not sure if this is correct, but 'installedSize' and
332 # 'zippedSize' are now in Bytes. Maybe blocks are needed?
333 # Well, Installer.app doesn't seem to care anyway, saying
334 # the installation needs 100+ MB...
335
336 numFiles = 0
337 installedSize = 0
338 zippedSize = 0
339
340 files = GlobDirectoryWalker(self.sourceFolder)
341 for f in files:
342 numFiles = numFiles + 1
343 installedSize = installedSize + os.lstat(f)[6]
344
345 try:
346 zippedSize = os.stat(self.archPath+ ".gz")[6]
347 except OSError: # ignore error
348 pass
349 base = self.packageInfo["Title"] + ".sizes"
350 f = open(join(self.packageResourceFolder, base), "w")
351 format = "NumFiles %d\nInstalledSize %d\nCompressedSize %d\n"
352 f.write(format % (numFiles, installedSize, zippedSize))
353
354
355 # Shortcut function interface
356
357 def buildPackage(*args, **options):
358 "A Shortcut function for building a package."
359
360 o = options
361 title, version, desc = o["Title"], o["Version"], o["Description"]
362 pm = PackageMaker(title, version, desc)
363 apply(pm.build, list(args), options)
364
365
366 ######################################################################
367 # Tests
368 ######################################################################
369
370 def test0():
371 "Vanilla test for the distutils distribution."
372
373 pm = PackageMaker("distutils2", "1.0.2", "Python distutils package.")
374 pm.build("/Users/dinu/Desktop/distutils2")
375
376
377 def test1():
378 "Test for the reportlab distribution with modified options."
379
380 pm = PackageMaker("reportlab", "1.10",
381 "ReportLab's Open Source PDF toolkit.")
382 pm.build(root="/Users/dinu/Desktop/reportlab",
383 DefaultLocation="/Applications/ReportLab",
384 Relocatable="YES")
385
386 def test2():
387 "Shortcut test for the reportlab distribution with modified options."
388
389 buildPackage(
390 "/Users/dinu/Desktop/reportlab",
391 Title="reportlab",
392 Version="1.10",
393 Description="ReportLab's Open Source PDF toolkit.",
394 DefaultLocation="/Applications/ReportLab",
395 Relocatable="YES")
396
397
398 ######################################################################
399 # Command-line interface
400 ######################################################################
401
402 def printUsage():
403 "Print usage message."
404
405 format = "Usage: %s <opts1> [<opts2>] <root> [<resources>]"
406 print format % basename(sys.argv[0])
407 print
408 print " with arguments:"
409 print " (mandatory) root: the package root folder"
410 print " (optional) resources: the package resources folder"
411 print
412 print " and options:"
413 print " (mandatory) opts1:"
414 mandatoryKeys = string.split("Title Version Description", " ")
415 for k in mandatoryKeys:
416 print " --%s" % k
417 print " (optional) opts2: (with default values)"
418
419 pmDefaults = PackageMaker.packageInfoDefaults
420 optionalKeys = pmDefaults.keys()
421 for k in mandatoryKeys:
422 optionalKeys.remove(k)
423 optionalKeys.sort()
424 maxKeyLen = max(map(len, optionalKeys))
425 for k in optionalKeys:
426 format = " --%%s:%s %%s"
427 format = format % (" " * (maxKeyLen-len(k)))
428 print format % (k, repr(pmDefaults[k]))
429
430
431 def main():
432 "Command-line interface."
433
434 shortOpts = ""
435 keys = PackageMaker.packageInfoDefaults.keys()
436 longOpts = map(lambda k: k+"=", keys)
437
438 try:
439 opts, args = getopt.getopt(sys.argv[1:], shortOpts, longOpts)
440 except getopt.GetoptError, details:
441 print details
442 printUsage()
443 return
444
445 optsDict = {}
446 for k, v in opts:
447 optsDict[k[2:]] = v
448
449 ok = optsDict.keys()
450 if not (1 <= len(args) <= 2):
451 print "No argument given!"
452 elif not ("Title" in ok and \
453 "Version" in ok and \
454 "Description" in ok):
455 print "Missing mandatory option!"
456 else:
457 apply(buildPackage, args, optsDict)
458 return
459
460 printUsage()
461
462 # sample use:
463 # buildpkg.py --Title=distutils \
464 # --Version=1.0.2 \
465 # --Description="Python distutils package." \
466 # /Users/dinu/Desktop/distutils
467
468
469 if __name__ == "__main__":
470 main()