ยปCore Development>Code coverage>Mac/scripts/buildpkg.py

Python code coverage for Mac/scripts/buildpkg.py

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