Created
April 13, 2016 15:55
-
-
Save jessepeterson/d9d1f592a8c54395827f73dc60b3a0f3 to your computer and use it in GitHub Desktop.
Create an Apple software distribution manifest for an Apple pkg installer (see http://help.apple.com/deployment/osx/#/ior5df10f73a)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python | |
# create a "wireless manifest" for Apple pkg installers. for details | |
# on the manifest see: http://help.apple.com/deployment/osx/#/ior5df10f73a | |
import subprocess | |
from tempfile import mkdtemp | |
import os | |
from xml.dom.minidom import parse, parseString | |
from hashlib import md5 | |
import plistlib | |
# use system PATH | |
XAR_PATH = 'xar' | |
MD5_CHUNK_SIZE = 1024 * 1024 * 10 # 10 MiB | |
def pkg_signed(filename): | |
xar_args = [XAR_PATH, | |
'-t', # only test archive | |
'--dump-toc=-', | |
'-f', | |
filename] | |
p = subprocess.Popen(xar_args, stdout=subprocess.PIPE) | |
toc, _ = p.communicate() | |
toc_md = parseString(toc) | |
# for purposes of checking just see if the xar TOC has an X509Certificate element | |
return len(toc_md.getElementsByTagName('X509Certificate')) >= 1 | |
def get_pkg_ids(filename): | |
'''Get metadata from Distribution or PackageInfo inside of pkg''' | |
tmp_dir = mkdtemp() | |
print 'Extracting Distribution/PackageInfo file to', tmp_dir | |
xar_args = [XAR_PATH, | |
'-x', # extract switch | |
'--exclude', '/', # exclude any files in subdirectories | |
'-C', tmp_dir, # extract to our temporary directory | |
'-f', filename, # extract this specific file | |
'Distribution', 'PackageInfo'] # files to extract | |
rtn = subprocess.call(xar_args) | |
tmp_dist_file = os.path.join(tmp_dir, 'Distribution') | |
tmp_pinf_file = os.path.join(tmp_dir, 'PackageInfo') | |
pkgs = [] | |
# for non-PackageInfo packages (use PackageInfo) | |
if os.path.exists(tmp_dist_file): | |
# use XML minidom to parse a Distribution file | |
dist_md = parse(tmp_dist_file) | |
# capture the pkg IDs and versions by searching for 'pkg-ref' elements | |
# which include a 'version' attribute on them. append them to our list | |
for i in dist_md.getElementsByTagName('pkg-ref'): | |
if i.hasAttribute('version'): | |
pkgs.append((i.getAttribute('id'), i.getAttribute('version'))) | |
# TODO: to ferret out any actual bundle IDs and use those? by logs for | |
# commerce/storehelperd daemon appear to look for packages by | |
# those. but then for packages that don't install bundles? | |
# dist_md.getElementsByTagName('bundle) | |
print 'Removing Distribution file' | |
os.unlink(tmp_dist_file) | |
# for non-Distribution packages (use the PackageInfo) | |
if os.path.exists(tmp_pinf_file): | |
pinf_md = parse(tmp_pinf_file) | |
# capture the pkg ID and version by searching for a pkg-info element | |
# and using the identifier and version attributes | |
for i in pinf_md.getElementsByTagName('pkg-info'): | |
pkgs.append((i.getAttribute('identifier'), i.getAttribute('version'))) | |
print 'Removing PackageInfo file' | |
os.unlink(tmp_pinf_file) | |
print 'Removing temp directory' | |
os.rmdir(tmp_dir) | |
return pkgs | |
def get_chunked_md5(filename, chunksize=10485760): | |
h = md5() | |
md5s = [] | |
with open(filename, 'rb') as f: | |
for chunk in iter(lambda: f.read(chunksize), b''): | |
new_hash = md5() | |
new_hash.update(chunk) | |
md5s.append(new_hash.hexdigest()) | |
return md5s | |
def gen_package_manifest(filename, url_prefix, title, subtitle=None): | |
asset = { | |
'kind': 'software-package', | |
'md5-size': MD5_CHUNK_SIZE, | |
'md5s': get_chunked_md5(filename, chunksize=MD5_CHUNK_SIZE), | |
'url': url_prefix + filename, | |
} | |
metadata = {'kind': 'software', 'title': title, 'sizeInBytes': os.path.getsize(filename)} | |
pkgs_ids = get_pkg_ids(filename) | |
pkgs_bundles = [{'bundle-identifier': i[0], 'bundle-version': i[1]} for i in pkgs_ids] | |
if subtitle: | |
metadata['subtitle'] = subtitle | |
metadata.update(pkgs_bundles[0]) | |
if len(pkgs_bundles) > 1: | |
metadata['items'] = pkgs_bundles | |
download = {'assets': [asset, ], 'metadata': metadata} | |
manifest = {'items': [download]} | |
return manifest | |
PKG = 'munkitools-2.3.1.2535.pkg' | |
# PKG = 'Server-3.2.2.pkg' | |
manif = gen_package_manifest(PKG, 'https://example.com/', 'Install Title') | |
plistlib.writePlist(manif, 'Manifest.xml') | |
print 'Wrote Manifest.xml' | |
if not pkg_signed(PKG): | |
print '>>> WARNING! WARNING! Package does not appear to be signed!' | |
else: | |
print 'Package appears to be signed (good) but did not check for valid signature or certificate' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment