-
Notifications
You must be signed in to change notification settings - Fork 8
/
make-adobe-cc-license-pkg
executable file
·253 lines (214 loc) · 10.6 KB
/
make-adobe-cc-license-pkg
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
#!/usr/bin/python
#pylint: disable=line-too-long
import Foundation
import optparse
import os
import plistlib
import re
import shutil
import subprocess
import sys
import tempfile
import xml.etree.ElementTree
from string import Template
from time import localtime
from distutils.version import LooseVersion
REVERSE_DOMAIN_DEFAULT = 'com.github.make-adobe-cc-license-pkg'
MUNKI_SUBDIR_DEFAULT = 'apps/Adobe/CC/Licenses'
def error_exit(msg, code=1):
'''Print a red error to stderr and exit with a given return code.'''
print >> sys.stderr, "\033[0;31m" + msg + "\033[0m"
sys.exit(code)
def print_status(msg):
'''Print a status message (in cyan).'''
print "\033[0;36m** " + msg + "\033[0m"
def main():
'''The main routine.'''
munkiimport_path = '/usr/local/munki/munkiimport'
usage = """%prog [options] <path/to/license_file_dir>
..where license_file_dir is the directory containing the license files output
by Creative Cloud Packager. helper.bin and prov.xml will be packaged and
an installer script written to handle installing them. An uninstaller script
will be output in the same directory as the pkg, and added to a Munki pkginfo
if --munki is used."""
parser = optparse.OptionParser(usage=usage)
munki = optparse.OptionGroup(parser, "Munki-related options")
munki.add_option('-m', '--munki', action='store_true', help=(
"Import the output package into Munki using Munki's admin tools. "
"The local copies of the package and uninstall script will be kept. "
"Configuration from the `munkiimport` tool will be used."))
munki.add_option('--repo-subdirectory', default=MUNKI_SUBDIR_DEFAULT,
help=("Munki repo destination subdirectory option (passed to munkiimport "
"--subdirectory). Defaults to '%s'.") % MUNKI_SUBDIR_DEFAULT)
parser.add_option_group(munki)
pkg_parser = optparse.OptionGroup(parser, "Package-related options")
pkg_parser.add_option('-n', '--name', help=(
"Package name (and name of pkginfo if --munki option used). "
"Defaults to the name of the directory containing the license "
"files."))
pkg_parser.add_option('-v', '--version', help=(
"Version string for created package. Defaults to today's date in the "
"format 'YYYY.MM.DD'"))
pkg_parser.add_option('-d', '--reverse-domain', help=(
"Reverse domain to prepend to the package name. Defaults to '%s'."
% REVERSE_DOMAIN_DEFAULT))
parser.add_option_group(pkg_parser)
parser.add_option('-o', '--output-dir', help=(
"Directory to output to. Defaults to the current working directory."))
parser.add_option('-r', '--remove-sw-tag', help=(
"Add the '--removeSWTag' adobe_prtk flag to the uninstaller, which "
"will remove the software entitlement tags upon deactivation."))
opts, args = parser.parse_args()
script_dir = os.path.abspath(os.path.dirname(__file__))
templates_dir = os.path.join(script_dir, 'script_templates')
if len(args) != 1:
error_exit("Please provide a single argument: the path to the "
"directory containing the license files output from CCP. "
"See -h for more details.")
license_files_dir = args[0]
prov_file = os.path.join(license_files_dir, 'prov.xml')
if not os.path.exists(prov_file):
error_exit("Prov file '%s' could not be found!" % prov_file)
try:
prov_obj = xml.etree.ElementTree.parse(prov_file)
except xml.etree.ElementTree.ParseError as err:
error_exit("Error parsing XML: %s" % err)
output_dir = os.getcwd()
if opts.output_dir:
if not os.access(opts.output_dir, os.W_OK):
error_exit("Output directory %s either does not exist or is not "
"writable." % opts.output_dir)
output_dir = opts.output_dir
if not opts.version:
today = localtime()
pkg_version = "%04d.%02d.%02d" % (
today.tm_year, today.tm_mon, today.tm_mday)
else:
pkg_version = opts.version
# adobe_prtk
aptee_path = os.path.join(license_files_dir, 'helper.bin')
if not os.path.exists(aptee_path) or not os.access(aptee_path, os.X_OK):
error_exit("helper.bin (adobe_prtk) at path %s exists, but is not "
"executable!" % aptee_path)
# Munki opts
if opts.munki:
if not os.access(munkiimport_path, os.X_OK):
error_exit("The --munki option requires the Munki admin tools "
"to be installed, but no `munkiimport` executable "
"found at %s" % munkiimport_path)
munki_prefs = Foundation.CFPreferencesCopyAppValue(
'repo_url', 'com.googlecode.munki.munkiimport')
if not munki_prefs:
error_exit("--munki option given, but Repo URL is not set. "
"Ensure your repo is properly configured "
"using `munkiimport --configure`")
# package params
pkg_name = opts.name or os.path.basename(os.path.dirname(prov_file))
reverse_domain = opts.reverse_domain or REVERSE_DOMAIN_DEFAULT
pkg_identifier = "%s.%s" % (reverse_domain, pkg_name)
# to pass the correct command flags to adobe_prtk, we need to know
# whether this is a device (teams) or serial (enterprise) license,
# based on whether there's a 'type' attribute of DBCS in the
# EnigmaData element
# - this is a list expression within a string, because this is what
# we will pass to our postinstall script template for substitute()
enigma_elem = prov_obj.find('EnigmaData')
if enigma_elem.get('type') == 'DBCS':
# DBCS is used for device licenses
prtk_install_options = """['--tool=GetPackagePools', '--provfile=/private/tmp/prov.xml']"""
print_status("Found DBCS (device) license type in prov.xml")
else:
# if type element not present, it's an enterprise/serial license
prtk_install_options = """['--tool=VolumeSerialize', '--provfile=/private/tmp/prov.xml']"""
print_status("Found serial number (enterprise) license type in prov.xml")
# Looking for something like:
# <CustomOverrides leid="V6{}CreativeCloudTeam-1.0-Mac-GM">
override_elem = prov_obj.find('CustomOverrides[@leid]')
if override_elem is None:
error_exit("Unable to extract an LEID from the prov XML. Expected to "
"find something in ./Provisioning/CustomOverrides")
leid = override_elem.get('leid')
print_status("Found LEID '%s' in prov.xml" % leid)
# get version from adobe_prtk by scanning for plist content
prtk_contents = open(aptee_path).read()
match = re.match(r'^.*?(<plist.*?\/plist>).*$', prtk_contents, re.DOTALL)
if match:
plist = plistlib.readPlistFromString(match.groups()[0])
else:
error_exit("Unable to extract a version from adobe_prtk.")
aptee_version = plist['CFBundleVersion']
print_status("Extracted version %s from adobe_prtk Info.plist section" %
aptee_version)
# set up the package root and some path variables
prtk_bin_payload_dir = '/usr/local/bin'
prtk_bin_dir_name = 'adobe_prtk_%s' % aptee_version
local_prtk_path = os.path.join(prtk_bin_payload_dir, prtk_bin_dir_name,
'adobe_prtk')
pkgroot = tempfile.mkdtemp()
private_tmp = os.path.join(pkgroot, 'private/tmp')
prtk_bin_dir = os.path.join(pkgroot + prtk_bin_payload_dir, prtk_bin_dir_name)
for pkgdir in [private_tmp, prtk_bin_dir]:
os.makedirs(pkgdir)
scripts_dir = tempfile.mkdtemp()
# copy our two payload files: adobe_prtk and prov.xml
shutil.copy(aptee_path, os.path.join(prtk_bin_dir, 'adobe_prtk'))
shutil.copy(prov_file, os.path.join(private_tmp, 'prov.xml'))
# add scripts from this repo's 'support' dir
support_bin_payload_dir = '/usr/local/bin/adobe_cc_license_pkg_support'
support_bin_dir = os.path.join(pkgroot + support_bin_payload_dir)
this_script_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
shutil.copytree(os.path.join(this_script_dir, 'support'), support_bin_dir)
# make postinstall script
postinstall_template = Template(open(os.path.join(templates_dir, 'postinstall')).read())
postinstall_contents = postinstall_template.substitute(
adobe_prtk_path=local_prtk_path,
adobe_prtk_options=prtk_install_options)
postinstall_file = os.path.join(scripts_dir, 'postinstall')
with open(postinstall_file, 'w') as postinstall:
postinstall.write(postinstall_contents)
os.chmod(postinstall_file, 0755)
# make uninstall script
prtk_uninstall_options = "['--tool=UnSerialize', '--leid=%s', '--deactivate'" % leid
# RemoveVolumeSerial added '--force', presumably to override new logic
# introduced in 10.x as part of:
# https://helpx.adobe.com/creative-cloud/kb/products-package-get-unserialized-unsubscribed.html
if LooseVersion(aptee_version) >= LooseVersion('10.0.0.36'):
prtk_uninstall_options += ", '--force'"
if opts.remove_sw_tag:
prtk_uninstall_options += ", '--removeSWTag']"
else:
prtk_uninstall_options += "]"
uninstall_template = Template(open(os.path.join(templates_dir, 'uninstall')).read())
uninstall_contents = uninstall_template.substitute(
adobe_prtk_dir=os.path.join(prtk_bin_payload_dir, prtk_bin_dir_name),
adobe_prtk_path=local_prtk_path,
adobe_prtk_options=prtk_uninstall_options,
identifier=pkg_identifier)
uninstall_script_path = os.path.join(output_dir, '%s-%s.uninstall' % (
pkg_name, pkg_version))
with open(uninstall_script_path, 'w') as uninstall:
uninstall.write(uninstall_contents)
os.chmod(uninstall_script_path, 0755)
print_status("Wrote uninstall script to %s" % uninstall_script_path)
# build it
pkg_output_path = os.path.join(output_dir, '%s-%s.pkg' % (pkg_name, pkg_version))
pkgbuild_cmd = ['/usr/bin/pkgbuild',
'--root', pkgroot,
'--scripts', scripts_dir,
'--version', pkg_version,
'--identifier', pkg_identifier,
pkg_output_path]
subprocess.call(pkgbuild_cmd)
print_status("Built package at %s" % pkg_output_path)
# import into Munki
if opts.munki:
print_status("Importing into Munki..")
munki_cmd = ['/usr/local/munki/munkiimport',
'--nointeractive',
'--subdirectory', opts.repo_subdirectory,
'--uninstall-script', uninstall_script_path,
pkg_output_path]
subprocess.call(munki_cmd)
print_status("Done.")
if __name__ == '__main__':
main()