Skip to content

Commit 3db5f18

Browse files
committed
added FoundationPlist since some apps store Info.plist in binary
1 parent fb3eb0f commit 3db5f18

File tree

2 files changed

+153
-13
lines changed

2 files changed

+153
-13
lines changed

quickpkg

Lines changed: 152 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,161 @@ import argparse
44
import string
55
import os
66
import subprocess
7-
import plistlib
87
import tempfile
98
import shutil
109
import stat
1110

11+
# includes FoundationPlist since some apps store their Info.plist
12+
# as binary PropertyLists
13+
14+
# FoundationPlist:
15+
16+
# Copyright 2009-2014 Greg Neagle.
17+
#
18+
# Licensed under the Apache License, Version 2.0 (the "License");
19+
# you may not use this file except in compliance with the License.
20+
# You may obtain a copy of the License at
21+
#
22+
# http://www.apache.org/licenses/LICENSE-2.0
23+
#
24+
# Unless required by applicable law or agreed to in writing, software
25+
# distributed under the License is distributed on an "AS IS" BASIS,
26+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
27+
# See the License for the specific language governing permissions and
28+
# limitations under the License.
29+
"""FoundationPlist.py -- a tool to generate and parse MacOSX .plist files.
30+
31+
This is intended as a drop-in replacement for Python's included plistlib,
32+
with a few caveats:
33+
- readPlist() and writePlist() operate only on a filepath,
34+
not a file object.
35+
- there is no support for the deprecated functions:
36+
readPlistFromResource()
37+
writePlistToResource()
38+
- there is no support for the deprecated Plist class.
39+
40+
The Property List (.plist) file format is a simple XML pickle supporting
41+
basic object types, like dictionaries, lists, numbers and strings.
42+
Usually the top level object is a dictionary.
43+
44+
To write out a plist file, use the writePlist(rootObject, filepath)
45+
function. 'rootObject' is the top level object, 'filepath' is a
46+
filename.
47+
48+
To parse a plist from a file, use the readPlist(filepath) function,
49+
with a file name. It returns the top level object (again, usually a
50+
dictionary).
51+
52+
To work with plist data in strings, you can use readPlistFromString()
53+
and writePlistToString().
54+
"""
55+
56+
from Foundation import NSData, \
57+
NSPropertyListSerialization, \
58+
NSPropertyListMutableContainersAndLeaves, \
59+
NSPropertyListXMLFormat_v1_0
60+
61+
62+
class FoundationPlistException(Exception):
63+
'''Base error for this module'''
64+
pass
65+
66+
67+
class NSPropertyListSerializationException(FoundationPlistException):
68+
'''Read error for this module'''
69+
pass
70+
71+
72+
class NSPropertyListWriteException(FoundationPlistException):
73+
'''Write error for this module'''
74+
pass
75+
76+
77+
# private functions
78+
def _dataToPlist(data):
79+
'''low-level function that parses a data object into a propertyList object'''
80+
darwin_vers = int(os.uname()[2].split('.')[0])
81+
if darwin_vers > 10:
82+
(plistObject, plistFormat, error) = (
83+
NSPropertyListSerialization.propertyListWithData_options_format_error_(
84+
data, NSPropertyListMutableContainersAndLeaves, None, None))
85+
else:
86+
# 10.5 doesn't support propertyListWithData:options:format:error:
87+
# 10.6's PyObjC wrapper for propertyListWithData:options:format:error:
88+
# is broken
89+
# so use the older NSPropertyListSerialization function
90+
(plistObject, plistFormat, error) = (
91+
NSPropertyListSerialization.propertyListFromData_mutabilityOption_format_errorDescription_(
92+
data, NSPropertyListMutableContainersAndLeaves, None, None))
93+
if plistObject is None:
94+
if error is None:
95+
error = "Plist data is invalid and could not be deserialized."
96+
raise NSPropertyListSerializationException(error)
97+
else:
98+
return plistObject
99+
100+
101+
def _plistToData(plistObject):
102+
'''low-level function that creates NSData from a plist object'''
103+
darwin_vers = int(os.uname()[2].split('.')[0])
104+
if darwin_vers > 10:
105+
(data, error) = (
106+
NSPropertyListSerialization.dataWithPropertyList_format_options_error_(
107+
plistObject, NSPropertyListXMLFormat_v1_0, 0, None))
108+
else:
109+
# use the older NSPropertyListSerialization function on 10.6 and 10.5
110+
(data, error) = (
111+
NSPropertyListSerialization.dataFromPropertyList_format_errorDescription_(
112+
plistObject, NSPropertyListXMLFormat_v1_0, None))
113+
if data is None:
114+
if error is None:
115+
error = "Property list invalid for format."
116+
raise NSPropertyListSerializationException(error)
117+
return data
118+
119+
120+
# public functions
121+
def readPlist(filepath):
122+
'''Read a .plist file from filepath. Return the unpacked root object
123+
(which is usually a dictionary).'''
124+
try:
125+
data = NSData.dataWithContentsOfFile_(filepath)
126+
except NSPropertyListSerializationException, error:
127+
# insert filepath info into error message
128+
errmsg = (u'%s in %s' % (error, filepath))
129+
raise NSPropertyListSerializationException(errmsg)
130+
return _dataToPlist(data)
131+
132+
133+
def readPlistFromString(aString):
134+
'''Read a plist data from a string. Return the root object.'''
135+
data = buffer(aString)
136+
return _dataToPlist(data)
137+
138+
139+
def writePlist(plistObject, filepath):
140+
'''Write 'plistObject' as a plist to filepath.'''
141+
plistData = _plistToData(plistObject)
142+
if plistData.writeToFile_atomically_(filepath, True):
143+
return
144+
else:
145+
raise NSPropertyListWriteException(
146+
u"Failed to write plist data to %s" % filepath)
12147

13-
quickpkg_version = '0.3'
14-
supported_extensions = ['dmg', 'app', 'zip']
15148

149+
def writePlistToString(plistObject):
150+
'''Create a plist-formatted string from plistObject.'''
151+
return str(_plistToData(plistObject))
152+
153+
154+
#
16155
# quickpkg
156+
#
157+
158+
159+
quickpkg_version = '0.4'
160+
supported_extensions = ['dmg', 'app', 'zip']
161+
17162

18163
# modeled after munkiimport but to build a pkg
19164

@@ -69,7 +214,7 @@ def dmg_has_sla(dmgpath):
69214
print "error getting imageinfo! %s, %s" % (result["return_code"], result["stderr"])
70215
return False
71216
result_plist = result["stdout"]
72-
imageinfo_dict = plistlib.readPlistFromString(result_plist)
217+
imageinfo_dict = readPlistFromString(result_plist)
73218
properties = imageinfo_dict.get('Properties')
74219
if properties is not None:
75220
has_sla = properties.get('Software License Agreement', False)
@@ -83,7 +228,7 @@ def attachdmg(dmgpath):
83228
if info_result["return_code"] == 0:
84229
# parse the plist output
85230
(theplist, alltext) = getFirstPlist(info_result["stdout"])
86-
info_dict = plistlib.readPlistFromString(theplist)
231+
info_dict = readPlistFromString(theplist)
87232
volpaths = []
88233
if "images" in info_dict.keys():
89234
for y in info_dict["images"]:
@@ -115,7 +260,7 @@ def attachdmg(dmgpath):
115260
if result["return_code"] == 0:
116261
# parse the plist output
117262
(theplist, alltext) = getFirstPlist(result["stdout"])
118-
resultdict = plistlib.readPlistFromString(theplist)
263+
resultdict = readPlistFromString(theplist)
119264
volpaths = []
120265
for x in resultdict["system-entities"]:
121266
if x["potentially-mountable"]:
@@ -157,7 +302,7 @@ def appNameAndVersion(app_path):
157302
print "Application at path %s does not have Info.plist" % app_path
158303
# TODO: cleanup volumes here
159304
cleanup_and_exit(1)
160-
info_plist = plistlib.readPlist(info_path)
305+
info_plist = readPlist(info_path)
161306
app_name = info_plist.get("CFBundleName")
162307
if app_name is None:
163308
app_name = info_plist.get("CFBundleDisplayName")

todo.txt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
- use some preference setting to determine default package name syntax
2-
√ add option to add scripts (--scripts, like the scripts folder)
3-
√ add option to add --preinstall script
4-
√ add option for --postinstall script
5-
√ support the --ownership flag
6-
√ option to output pkg to a different dir than cwd
72
- signature support
83
- identify shell scripts and build a payload free package
9-
- identify mobileconfigs and build a package installer if make-profile-pkg is present
104
- support for tar, gzip and bzip
115
? other possible file formats: fonts, prefpanes, Safari extensions?
126
? identify app just by name or id (could use: mdfind "kMDItemKind == 'Application' && kMDItemDisplayName == 'iTunes'")
7+
? identify mobileconfigs and build a package installer if make-profile-pkg is present

0 commit comments

Comments
 (0)