├── APInfo
├── APInfo
├── APInfo.png
└── README.md
├── README.md
├── SippySIP
└── sippysip.py
└── makexcodesimulators
└── makexcodesimulators.py
/APInfo/APInfo:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | """Looks up an iOS or MAS application's name and optionally displays
3 | version, release notes and release date. This can only be used to upload the
4 | data to a slack channel.
5 |
6 | Author: Erik Gomez
7 | """
8 |
9 | import json
10 | import optparse
11 | import re
12 | import subprocess
13 | import sys
14 | import urllib2
15 |
16 |
17 | def get_results(id):
18 | url = 'https://itunes.apple.com/lookup?id=' + id
19 | try:
20 | request = urllib2.urlopen(url)
21 | jsondata = json.loads(request.read())
22 | return jsondata['results'][0]
23 | except:
24 | raise ProcessorError('Could not retrieve version from URL: %s' % url)
25 |
26 |
27 | def post_to_slack(appname, outputdata, slackchannel, slackicon, slackusername,
28 | slackwebhook):
29 | # Slack payload
30 | payload = {
31 | "channel": slackchannel,
32 | "username": slackusername,
33 | "icon_url": slackicon,
34 | "attachments": [
35 | {
36 | 'fields': [
37 | {
38 | 'title': appname,
39 | 'value': outputdata
40 | }
41 | ]
42 | }
43 | ]
44 | }
45 | try:
46 | cmd = ['/usr/bin/curl', '-X', 'POST', '--data-urlencode',
47 | 'payload=' + json.dumps(payload), slackwebhook]
48 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
49 | stderr=subprocess.PIPE)
50 | output, err = proc.communicate()
51 | except Exception:
52 | sys.stderr.write('Failed to send to Slack: %s') % payload
53 | sys.exit(1)
54 |
55 |
56 | def main():
57 | usage = "%prog [options]"
58 | o = optparse.OptionParser(usage=usage)
59 | o.add_option("--id", help=("Required: iTunes Application ID."))
60 | o.add_option("--expectedversion", default=None,
61 | help=("Optional: Specify expected version. Useful when using "
62 | "Slack output."))
63 | o.add_option("--releasedate", action="store_true", default=False,
64 | help=("Optional: Obtain Release Date information"))
65 | o.add_option("--releasenotes", action="store_true", default=False,
66 | help=("Optional: Obtain Release Notes information."))
67 | o.add_option("--minimumos", action="store_true", default=False,
68 | help=("Optional: Obtain Minimum OS information."))
69 | o.add_option("--slack", action="store_true", default=False,
70 | help=("Optional: Use Slack"))
71 | o.add_option("--slackwebhook", default=None,
72 | help=("Optional: Slack Webhook URL. Requires Slack Option."))
73 | o.add_option("--slackusername", default=None,
74 | help=("Optional: Slack username. Requires Slack Option."))
75 | o.add_option("--slackchannel", default=None,
76 | help=("Optional: Slack channel. Requires Slack Option."))
77 | o.add_option("--version", action="store_true", default=False,
78 | help=("Optional: Obtain Version information."))
79 | opts, args = o.parse_args()
80 |
81 | if not opts.id:
82 | o.print_help()
83 | sys.exit(1)
84 |
85 | # Variables
86 | id = opts.id
87 | version = opts.version
88 | releasenotes = opts.releasenotes
89 | releasedate = opts.releasedate
90 | minos = opts.minimumos
91 | slack = opts.slack
92 | slackwebhook = opts.slackwebhook
93 | slackusername = opts.slackusername
94 | slackchannel = opts.slackchannel
95 | slackicon = get_results(id)['artworkUrl100']
96 | if 'mac-software' in get_results(id)['kind']:
97 | appname = ('Application: %s (macOS)' % get_results(id)['trackName'])
98 | else:
99 | appname = ('Application: %s (iOS)' % get_results(id)['trackName'])
100 | # Hacky way to dynamically generate output data with newlines.
101 | if minos is True:
102 | minos = '\nMinimum OS: %s' % get_results(id)['minimumOsVersion']
103 | if version is True:
104 | version = '\nVersion: %s' % get_results(id)['version']
105 | if releasenotes is True:
106 | try:
107 | releasenotes = '\nRelease Notes: %s' % get_results(id)['releaseNotes']
108 | except Exception:
109 | # If no release notes, use description:
110 | releasenotes = '\nRelease Notes: %s' % get_results(id)['description']
111 | sys.stderr.write('Failed to obtain Release Notes.')
112 | if releasedate is True:
113 | releasedate = ('\nRelease Date: %s'
114 | % get_results(id)['currentVersionReleaseDate'][0:10])
115 | datatypes = [minos, version, releasedate, releasenotes]
116 | outputdata = ' '.join(filter(None, datatypes))
117 |
118 | # Start the work
119 | if opts.expectedversion:
120 | expectedversion = opts.expectedversion
121 | if expectedversion == get_results(id)['version']:
122 | print 'Found expected version for %s. Exiting.' % appname
123 | sys.exit(1)
124 | else:
125 | print 'New version of application detected. \n'
126 | print appname + outputdata
127 | if slack is True:
128 | post_to_slack(appname, outputdata, slackchannel, slackicon,
129 | slackusername, slackwebhook)
130 | else:
131 | print appname + outputdata
132 | if slack is True:
133 | post_to_slack(appname, outputdata, slackchannel, slackicon,
134 | slackusername, slackwebhook)
135 |
136 | if __name__ == '__main__':
137 | main()
138 |
--------------------------------------------------------------------------------
/APInfo/APInfo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikng/scripts/d17d205cecb3bf1faf6c10d9aa010c4444b93b14/APInfo/APInfo.png
--------------------------------------------------------------------------------
/APInfo/README.md:
--------------------------------------------------------------------------------
1 | ## APInfo
2 |
3 | APInfo allows you to obtain information about iOS/macOS applications and optionally output the results to slack.
4 |
5 | ### Commands
6 | ```
7 | Usage: APInfo [options]
8 |
9 | Options:
10 | -h, --help show this help message and exit
11 | --id=ID Required: iTunes Application ID.
12 | --expectedversion=EXPECTEDVERSION
13 | Optional: Specify expected version. Useful when using
14 | Slack output.
15 | --releasedate Optional: Obtain Release Date information
16 | --releasenotes Optional: Obtain Release Notes information.
17 | --slack Optional: Use Slack
18 | --slackwebhook=SLACKWEBHOOK
19 | Optional: Slack Webhook URL. Requires Slack Option.
20 | --slackusername=SLACKUSERNAME
21 | Optional: Slack username. Requires Slack Option.
22 | --slackchannel=SLACKCHANNEL
23 | Optional: Slack channel. Requires Slack Option.
24 | --version Optional: Obtain Version information.
25 | ```
26 | ### Examples
27 | ##### Application Info
28 | At the bare minimum, the `id` for the Application must be passed.
29 |
30 | Example:
31 | ```
32 | Application: Keynote for macOS
33 | URL: https://itunes.apple.com/us/app/keynote/id409183694?mt=12
34 | ID: 409183694
35 |
36 | ./APInfo \
37 | --id 409183694
38 | Application: Keynote (macOS)
39 | ```
40 |
41 | ##### Additional Application Info
42 | APInfo can optionally return the Release Date, Release Notes and version of the application.
43 |
44 | Example:
45 | ```
46 | ./APInfo \
47 | --id 409183694 \
48 | --releasedate \
49 | --releasenotes \
50 | --version
51 |
52 | Application: Keynote (macOS)
53 | Version: 6.6.2
54 | Release Date: 2016-05-10
55 | Release Notes: This update contains stability improvements and bug fixes.
56 | ```
57 | ##### Expected Version
58 | If you plan to wrap APInfo with another tool, you may want to pass an expected version. This is useful for integrating with Slack to reduce the number of POSTS.
59 |
60 | Example:
61 | ```
62 | ./APInfo \
63 | --id 409183694 \
64 | --expectedversion "6.6.2"
65 |
66 | Found expected version for Application: Keynote (macOS). Exiting.
67 | ```
68 |
69 | ##### Uploading to Slack
70 | If you would like to optionally upload your results to slack, you must pass _all_ slack parameters:
71 |
72 | A webhook is also required. For more information, please see [Slack's documentation](https://api.slack.com/incoming-webhooks).
73 | ```
74 | ./APInfo \
75 | --id 409183694 \
76 | --releasedate \
77 | --releasenotes \
78 | --version \
79 | --slack \
80 | --slackchannel "#test" \
81 | --slackusername "APInfo" \
82 | --slackwebhook "https://hooks.slack.com/services/yourwebhookurl"
83 | ```
84 |
85 | The slack output will conditionally use the Application's 100px icon for the bot's icon_url.
86 |
87 | 
88 |
89 | ##### Additional notes on Slack
90 | If you plan to wrap this application, note that there is a bug with Slack where only the first message from a bot will show the icon. To workaround this, conditionally or statically set the `--slackusername` to unique values per application.
91 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Readme
2 | Public Scripts.
3 |
--------------------------------------------------------------------------------
/SippySIP/sippysip.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Sippy SIP - python wrapper for csrutil
5 | # User notifications designed around Yo
6 |
7 | # Written by Erik Gomez
8 | from CoreFoundation import CFPreferencesCopyAppValue, CFPreferencesSetAppValue
9 | from Foundation import NSDate, NSMutableArray
10 | from SystemConfiguration import SCDynamicStoreCopyConsoleUser
11 | import os
12 | import platform
13 | import plistlib
14 | import shutil
15 | import subprocess
16 | import time
17 |
18 | # Global Variables
19 | # Make sure sippysipLAId variables matches the Label in the
20 | # sippySIPLaunchAgent variable
21 | sippysipLAId = 'com.github.erikng.sippysipagent'
22 | # Path to the plist sippysip writes to track each time there is an event
23 | # where sippysip had to fix the device.
24 | writePlistPath = '/Library/Application Support/LogEvents/sippysip.plist'
25 |
26 |
27 | sippySIPLaunchAgent = """
28 |
29 |
30 |
31 | Label
32 | com.github.erikng.sippysipagent
33 | ProgramArguments
34 |
35 | /usr/bin/python
36 | /Library/Application Support/sippysip/sippysipagent.py
37 |
38 | RunAtLoad
39 |
40 |
41 |
42 | """
43 |
44 |
45 | sippySIPAgentScript = """#!/usr/bin/python
46 | # -*- coding: utf-8 -*-
47 | import subprocess
48 |
49 | # Path to Yo binary
50 | yopath = '/Applications/Utilities/yo.app/Contents/MacOS/yo'
51 |
52 | def yo_single_button(yopath, title, informtext, accepttext, declinetext,
53 | script):
54 | try:
55 | cmd = [
56 | yopath, # path to you
57 | '-t', title, # title
58 | '-n', informtext, # subtext
59 | '-b', accepttext, # accept button
60 | '-B', script, # accept button script action
61 | '-o', declinetext, # decline button
62 | '-d' # ignore do-not-disturb mode
63 | ]
64 | proc = subprocess.Popen(
65 | cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
66 | output, err = proc.communicate()
67 | return output
68 | except Exception:
69 | return None
70 |
71 |
72 | def touch(path):
73 | try:
74 | touchFile = ['/usr/bin/touch', path]
75 | proc = subprocess.Popen(touchFile, stdout=subprocess.PIPE,
76 | stderr=subprocess.PIPE)
77 | touchFileOutput, err = proc.communicate()
78 | os.chmod(path, 0777)
79 | return touchFileOutput
80 | except Exception:
81 | return None
82 |
83 |
84 | def main():
85 | # Send a yo notification
86 | global yopath
87 | yo_single_button(yopath, 'Mind rebooting?', 'We detected that your device '
88 | 'is in a misconfigured state.', 'Restart Now', 'Restart Later',
89 | r\"\"\"osascript -e \'tell app "loginwindow" to «event aevtrrst»\'\"\"\")
90 |
91 | # Touch our sippysip watch path.
92 | touch('/Users/Shared/.sippysip')
93 |
94 |
95 | if __name__ == '__main__':
96 | main()
97 |
98 | """
99 |
100 |
101 | def cleanUp(sippysipPath, sippysipLAPath, sippysipLAId, userId,
102 | sippysipWatchPath):
103 | # Attempt to remove the LaunchAgent
104 | SippySIPLog('Attempting to remove LaunchAgent: ' + sippysipLAPath)
105 | try:
106 | os.remove(sippysipLAPath)
107 | except: # noqa
108 | pass
109 |
110 | # Attempt to remove the trigger
111 | if os.path.isfile(sippysipWatchPath):
112 | SippySIPLog('Attempting to remove trigger: ' + sippysipWatchPath)
113 | try:
114 | os.remove(sippysipWatchPath)
115 | except: # noqa
116 | pass
117 |
118 | # Attempt to remove the launchagent from the user's list
119 | SippySIPLog('Targeting user id for LaunchAgent removal: ' + userId)
120 | SippySIPLog('Attempting to remove LaunchAgent: ' + sippysipLAId)
121 | launchCTL('/bin/launchctl', 'asuser', userId,
122 | '/bin/launchctl', 'remove', sippysipLAId)
123 |
124 | # Attempt to kill SippySIP's path
125 | SippySIPLog('Attempting to remove sippysip directory: ' + sippysipPath)
126 | try:
127 | shutil.rmtree(sippysipPath)
128 | except: # noqa
129 | pass
130 |
131 |
132 | def getConsoleUser():
133 | CFUser = SCDynamicStoreCopyConsoleUser(None, None, None)
134 | return CFUser
135 |
136 |
137 | def launchCTL(*arg):
138 | # Use *arg to pass unlimited variables to command.
139 | cmd = arg
140 | run = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
141 | output, err = run.communicate()
142 | return output
143 |
144 |
145 | def csrutil(command):
146 | cmd = ['/usr/bin/csrutil', command]
147 | run = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
148 | output, err = run.communicate()
149 | if command == 'status':
150 | if 'System Integrity Protection status: disabled.' in output:
151 | return True
152 | else:
153 | return False
154 | elif command == 'clear':
155 | if 'Successfully cleared System Integrity Protection.' in output:
156 | return True
157 | else:
158 | return False
159 |
160 |
161 | def nvram():
162 | cmd = ['/usr/sbin/nvram', '-p']
163 | run = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
164 | output, err = run.communicate()
165 | if 'csr-active-config' in output:
166 | return True
167 | else:
168 | return False
169 |
170 |
171 | def writePlist(timestamp, writePlistPath):
172 | sippysip = []
173 | if os.path.isfile(writePlistPath):
174 | sippysip = CFPreferencesCopyAppValue('Events', writePlistPath)
175 | if sippysip:
176 | sippysip = NSMutableArray.alloc().initWithArray_(sippysip)
177 | else:
178 | sippysip = []
179 | sippysip.append(timestamp)
180 | CFPreferencesSetAppValue('Events', sippysip, writePlistPath)
181 | else:
182 | sippysip.append(timestamp)
183 | CFPreferencesSetAppValue('Events', sippysip, writePlistPath)
184 |
185 |
186 | def getOSVersion():
187 | """Return OS version."""
188 | return platform.mac_ver()[0]
189 |
190 |
191 | def SippySIPLog(text):
192 | logPath = '/private/var/log/sippysip.log'
193 | formatStr = '%b %d %Y %H:%M:%S %z: '
194 | logevent = time.strftime(formatStr) + text
195 | print logevent
196 | with open(logPath, 'a+') as log:
197 | log.write(logevent + '\n')
198 |
199 |
200 | def main():
201 | # State variables
202 | global sippySIPLaunchAgent
203 | global sippySIPAgentScript
204 | global sippysipLAId
205 | global writePlistPath
206 | currentUserUid = getConsoleUser()
207 | userId = str(getConsoleUser()[1])
208 | pendingReboot = False
209 |
210 | # Check SIP Status
211 | SippySIPLog('Checking SIP State...')
212 | sipCsrutilDisabled = csrutil('status')
213 | SippySIPLog('Checking NVRAM SIP State...')
214 | sipNVRAMDisabled = nvram()
215 |
216 | # If SIP is disabled, we need to do $things.
217 | if sipCsrutilDisabled:
218 | SippySIPLog('Detected SIP Disabled via csrutil. Checking against '
219 | 'NVRAM entries...')
220 | if sipNVRAMDisabled:
221 | SippySIPLog('Detected SIP Disabled via NVRAM.')
222 | SippySIPLog('Attempting to Re-Enable SIP...')
223 | sipCsrutilClear = csrutil('clear')
224 | if sipCsrutilClear:
225 | SippySIPLog('SIP Re-Enabled - Logging event to plist.')
226 | timestamp = NSDate.date()
227 | sippysipPlist = writePlist(timestamp, writePlistPath)
228 | pendingReboot = True
229 | else:
230 | SippySIPLog('Detected SIP Enabled via NVRAM. Device pending '
231 | 'reboot...')
232 | pendingReboot = True
233 | # If csrutil says things are cool, let's just validate against NVRAM.
234 | else:
235 | SippySIPLog('Detected SIP Enabled via csrutil. Checking against '
236 | 'NVRAM entries...')
237 | # Some kind of fuckery is going on here, so let's clear it and log.
238 | if sipNVRAMDisabled:
239 | SippySIPLog('Detected SIP Disabled via NVRAM.')
240 | SippySIPLog('Attempting to Re-Enable SIP...')
241 | sipCsrutilClear = csrutil('clear')
242 | if sipCsrutilClear:
243 | SippySIPLog('SIP Re-Enabled - Logging event to plist.')
244 | timestamp = NSDate.date()
245 | sippysipPlist = writePlist(timestamp, writePlistPath)
246 | pendingReboot = True
247 | else:
248 | SippySIPLog('SIP has been validated and is enabled.')
249 | exit(0)
250 |
251 | # If we are pending reboot, we should send a Yo notification to the user
252 | # informing them that it's time to reboot.
253 | if pendingReboot:
254 | SippySIPLog('Device is pending reboot - triggering user alert.')
255 | if (currentUserUid[0] is None or currentUserUid[0] == u'loginwindow'
256 | or currentUserUid[0] == u'_mbsetupuser'):
257 | SippySIPLog('No user logged in - Skipping Yo notification...')
258 | else:
259 | # sippysip's agent variables
260 | sippysipLAPlist = sippysipLAId + '.plist'
261 | sippysipPath = os.path.join('/Library', 'Application Support',
262 | 'sippysip')
263 | sippysipAgentPath = os.path.join(sippysipPath, 'sippysipagent.py')
264 | sippysipLAPath = os.path.join('/Library', 'LaunchAgents',
265 | sippysipLAPlist)
266 | sippysipWatchPath = '/Users/Shared/.sippysip'
267 |
268 | # Create sippysip's agent folder, script and agent.
269 | SippySIPLog('Creating sippysip agent folder and files...')
270 | if not os.path.isdir(sippysipPath):
271 | os.makedirs(sippysipPath)
272 | with open(sippysipAgentPath, 'wb') as na:
273 | na.write(sippySIPAgentScript)
274 | with open(sippysipLAPath, 'wb') as la:
275 | la.write(sippySIPLaunchAgent)
276 |
277 | # Turn on sippysip's agent
278 | SippySIPLog('Loading sippysip agent...')
279 | launchCTL('/bin/launchctl', 'asuser', userId,
280 | '/bin/launchctl', 'load', sippysipLAPath)
281 |
282 | # Wait for sippysip's agent to complete
283 | while not os.path.isfile(sippysipWatchPath):
284 | SippySIPLog('Waiting for the trigger file...')
285 | time.sleep(0.5)
286 |
287 | # Clean this shit up.
288 | SippySIPLog('Cleaning up sippysip agent folder and files...')
289 | cleanUp(sippysipPath, sippysipLAPath, sippysipLAId, userId,
290 | sippysipWatchPath)
291 |
292 |
293 | if __name__ == '__main__':
294 | main()
295 |
--------------------------------------------------------------------------------
/makexcodesimulators/makexcodesimulators.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # encoding: utf-8
3 |
4 | # Written by Erik Gomez
5 | # Lots of code and functions taken from installinstallmacos.py
6 |
7 | #
8 | # Thanks to Greg Neagle for most of the working/good code.
9 | #
10 |
11 | '''makexcodesimulators.py
12 | A tool to download and create distribution packages to properly install
13 | xcode simulators via your tool of choice.
14 |
15 | This has only been tested on Xcode 9.3 so YMMV'''
16 |
17 |
18 | import argparse
19 | import os
20 | import plistlib
21 | import shutil
22 | import subprocess
23 | import sys
24 | import tempfile
25 | import urlparse
26 | from xml.parsers.expat import ExpatError
27 | sys.path.append('/usr/local/munki')
28 | from munkilib import FoundationPlist # noqa
29 |
30 |
31 | class ReplicationError(Exception):
32 | '''A custom error when replication fails'''
33 | pass
34 |
35 |
36 | DISTRIBUTIONPLIST = """
37 | """
38 |
39 |
40 | def get_xcode_info(xcodepath):
41 | keys_to_get = ['DVTPlugInCompatibilityUUID', 'DTXcode']
42 | keys_obtained = {}
43 | xcode_info_plist_path = os.path.join(xcodepath, 'Contents/Info.plist')
44 | xcode_info_plist = FoundationPlist.readPlist(xcode_info_plist_path)
45 | for xcode_key in keys_to_get:
46 | if xcode_key in xcode_info_plist:
47 | if xcode_key == 'DTXcode':
48 | xcode_key_value = xcode_info_plist[xcode_key]
49 | # You get something back like 0930
50 | if xcode_key_value.startswith('0'):
51 | # We strip the first character to end up with 930
52 | xcode_key_value = xcode_key_value[1:]
53 | # Now we take 930, convert to a list and then join it. This
54 | # will give us 9.3.0
55 | xcode_key_value = '.'.join(list(xcode_key_value))
56 | # Return the value
57 | keys_obtained[xcode_key] = xcode_key_value
58 | else:
59 | keys_obtained[xcode_key] = xcode_info_plist[xcode_key]
60 | return keys_obtained
61 |
62 |
63 | def replicate_url(full_url, temp_dir, show_progress=False):
64 | relative_url = full_url.split('/')[-1]
65 | local_file_path = os.path.join(temp_dir, relative_url)
66 | if show_progress:
67 | options = '-fL'
68 | else:
69 | options = '-sfL'
70 | curl_cmd = ['/usr/bin/curl', options, '--create-dirs',
71 | '-o', local_file_path]
72 | if os.path.exists(local_file_path):
73 | curl_cmd.extend(['-z', local_file_path])
74 | curl_cmd.append(full_url)
75 | print "Downloading %s to %s..." % (full_url, relative_url)
76 | try:
77 | subprocess.check_call(curl_cmd)
78 | except subprocess.CalledProcessError, err:
79 | raise ReplicationError(err)
80 | return local_file_path
81 |
82 |
83 | def download_and_parse_xcode_catalog(temp_dir, xcode_version, xcode_uuid):
84 | url = 'https://devimages-cdn.apple.com/downloads/xcode/simulators/index-' \
85 | + xcode_version + '-' + xcode_uuid + '.dvtdownloadableindex'
86 | try:
87 | xcode_catalog_path = replicate_url(url, temp_dir, show_progress=False)
88 | except ReplicationError, err:
89 | print >> sys.stderr, 'Could not replicate %s: %s' % (url, err)
90 | exit(-1)
91 | try:
92 | catalog = plistlib.readPlist(xcode_catalog_path)
93 | downloadable_simulators = []
94 | for simulator in catalog['downloadables']:
95 | pkg_identifier = simulator['identifier'].split(
96 | '$(DOWNLOADABLE_VERSION_MAJOR)_$(DOWNLOADABLE_VERSION_MINOR)'
97 | )[0]
98 | pkg_version = simulator['version']
99 | simulator_type = pkg_identifier.split('com.apple.pkg.')[1]
100 | major_version = pkg_version.split('.')[0]
101 | minor_version = pkg_version.split('.')[1]
102 | simulator_version = major_version + '.' + minor_version
103 | url = 'https://devimages-cdn.apple.com/downloads/xcode/'\
104 | 'simulators/' + pkg_identifier + major_version + '_' + \
105 | minor_version + '-' + pkg_version + '.dmg'
106 | if 'TV' in simulator_type:
107 | simulator_runtime = 'tvOS'
108 | elif 'iPhone' in simulator_type:
109 | simulator_runtime = 'iOS'
110 | elif 'Watch' in simulator_type:
111 | simulator_runtime = 'watchOS'
112 | downloadable_simulators.append(
113 | {
114 | 'download_url': url,
115 | 'major_version': major_version,
116 | 'minor_version': minor_version,
117 | 'pkg_identifier': pkg_identifier,
118 | 'pkg_version': pkg_version,
119 | 'simulator_runtime': simulator_runtime,
120 | 'simulator_type': simulator_type,
121 | 'simulator_version': simulator_version
122 | }
123 | )
124 | return downloadable_simulators
125 | except (OSError, IOError, ExpatError), err:
126 | print >> sys.stderr, (
127 | 'Error reading %s: %s' % (xcode_catalog_path, err))
128 | exit(1)
129 |
130 |
131 | def replicate_package(url, temp_dir):
132 | try:
133 | dmg_url = replicate_url(url, temp_dir, show_progress=True)
134 | return dmg_url
135 | except ReplicationError, err:
136 | print >> sys.stderr, (
137 | 'Could not replicate %s: %s' % (package['URL'], err))
138 | exit(-1)
139 |
140 |
141 | def mountdmg(dmgpath):
142 | """
143 | Attempts to mount the dmg at dmgpath and returns first mountpoint
144 | """
145 | mountpoints = []
146 | dmgname = os.path.basename(dmgpath)
147 | cmd = ['/usr/bin/hdiutil', 'attach', dmgpath,
148 | '-mountRandom', '/tmp', '-nobrowse', '-plist',
149 | '-owners', 'on']
150 | proc = subprocess.Popen(cmd, bufsize=-1,
151 | stdout=subprocess.PIPE, stderr=subprocess.PIPE)
152 | (pliststr, err) = proc.communicate()
153 | if proc.returncode:
154 | print >> sys.stderr, 'Error: "%s" while mounting %s.' % (err, dmgname)
155 | return None
156 | if pliststr:
157 | plist = plistlib.readPlistFromString(pliststr)
158 | for entity in plist['system-entities']:
159 | if 'mount-point' in entity:
160 | mountpoints.append(entity['mount-point'])
161 |
162 | return mountpoints[0]
163 |
164 |
165 | def unmountdmg(mountpoint):
166 | """
167 | Unmounts the dmg at mountpoint
168 | """
169 | proc = subprocess.Popen(['/usr/bin/hdiutil', 'detach', mountpoint],
170 | bufsize=-1, stdout=subprocess.PIPE,
171 | stderr=subprocess.PIPE)
172 | (dummy_output, err) = proc.communicate()
173 | if proc.returncode:
174 | print >> sys.stderr, 'Polite unmount failed: %s' % err
175 | print >> sys.stderr, 'Attempting to force unmount %s' % mountpoint
176 | # try forcing the unmount
177 | retcode = subprocess.call(['/usr/bin/hdiutil', 'detach', mountpoint,
178 | '-force'])
179 | if retcode:
180 | print >> sys.stderr, 'Failed to unmount %s' % mountpoint
181 |
182 |
183 | def create_distribution_package(dxml_path, temp_mount_path, output_dir,
184 | pkg_name):
185 | pkg_file_name = pkg_name + '_dist.pkg'
186 | pkg_output_path = os.path.join(output_dir, pkg_file_name)
187 | cmd = ['/usr/bin/productbuild', '--distribution', dxml_path,
188 | '--resources', '.', pkg_output_path]
189 | try:
190 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
191 | stderr=subprocess.PIPE, cwd=temp_mount_path)
192 | output, err = proc.communicate()
193 | return True
194 | except subprocess.CalledProcessError, err:
195 | print >> sys.stderr, err
196 | return False
197 |
198 |
199 | def main():
200 | parser = argparse.ArgumentParser()
201 | parser.add_argument('--xcodepath', default='/Applications/Xcode.app',
202 | help='Required: Path to Xcode app.')
203 | parser.add_argument('--outputdir', metavar='path_to_working_dir',
204 | default='/Users/Shared/makexcodesimulators',
205 | help='Path to output packages.')
206 | args = parser.parse_args()
207 |
208 | xcodepath = args.xcodepath
209 | outputpath = args.outputdir
210 | if not os.path.isdir(outputpath):
211 | os.makedirs(outputpath)
212 |
213 | if not os.path.isdir(xcodepath):
214 | print 'Xcode path was not found: %s' % xcodepath
215 | exit(1)
216 | else:
217 | global DISTRIBUTIONPLIST
218 | temp_dir = tempfile.mkdtemp()
219 | print 'Temporary directory: %s' % str(temp_dir)
220 | xcode_app_info = get_xcode_info(xcodepath)
221 | if len(xcode_app_info) == 2:
222 | xcode_version = xcode_app_info['DTXcode']
223 | xcode_uuid = xcode_app_info['DVTPlugInCompatibilityUUID']
224 | downloadable_simulators = download_and_parse_xcode_catalog(
225 | temp_dir, xcode_version, xcode_uuid)
226 | sorted_downloadabled_simulators = sorted(downloadable_simulators)
227 | if not downloadable_simulators:
228 | print >> sys.stderr, (
229 | 'No Xcode simulators found in catalog.')
230 | exit(1)
231 |
232 | # display a menu of choices
233 | print '%2s %12s %10s %s' % ('#', 'SimulatorType', 'Version',
234 | 'URL')
235 | for index, simulator_info in enumerate(
236 | sorted_downloadabled_simulators):
237 | print '%2s %12s %10s %s' % (
238 | index+1, simulator_info['simulator_type'],
239 | simulator_info['simulator_version'],
240 | simulator_info['download_url'])
241 |
242 | answer = raw_input(
243 | '\nChoose a product to download (1-%s): ' % len(
244 | downloadable_simulators))
245 | try:
246 | id = int(answer) - 1
247 | if id < 0:
248 | raise ValueError
249 | simulator_chosen = sorted_downloadabled_simulators[id]
250 | except (ValueError, IndexError):
251 | print 'Exiting.'
252 | exit(0)
253 |
254 | # download the package for the selected product
255 | simulator_dmg_path = replicate_package(
256 | simulator_chosen['download_url'], temp_dir)
257 |
258 | # mount the dmg so we can make our new package
259 | print 'Mounting dmg at: %s' % str(simulator_dmg_path)
260 | mountpoint = mountdmg(simulator_dmg_path)
261 |
262 | if mountpoint:
263 | plist_title = simulator_chosen['simulator_type'] + \
264 | simulator_chosen['major_version'] + '_' + \
265 | simulator_chosen['minor_version']
266 | plist_pkg_relative_path = plist_title + '.pkg'
267 | plist_pkg_ref = simulator_chosen['pkg_identifier'] + \
268 | simulator_chosen['major_version'] + '_' + \
269 | simulator_chosen['minor_version']
270 | plist_pkg_version = simulator_chosen['pkg_version']
271 | plist_runtime_path = '/Library/Developer/CoreSimulator/'\
272 | 'Profiles/Runtimes/%s.simruntime' % (
273 | simulator_chosen['simulator_runtime'] + ' ' +
274 | simulator_chosen['simulator_version'])
275 | plist_sdk_version = simulator_chosen['simulator_type'] + \
276 | simulator_chosen['major_version'] + '_' + \
277 | simulator_chosen['minor_version']
278 |
279 | DISTRIBUTIONPLIST += '\n' + ' '\
280 | '\"%s\"' % (plist_title)
281 | DISTRIBUTIONPLIST += '\n' + ' '\
282 | '' % (plist_pkg_ref)
283 | DISTRIBUTIONPLIST += '\n' + ' '\
284 | ''
285 | DISTRIBUTIONPLIST += '\n' + ' '\
286 | ''
287 | DISTRIBUTIONPLIST += '\n' + ' '\
288 | '' % (plist_pkg_ref)
289 | DISTRIBUTIONPLIST += '\n' + ' '\
290 | ''
291 | DISTRIBUTIONPLIST += '\n' + ' '\
292 | '' % (plist_pkg_ref, plist_sdk_version,
294 | plist_runtime_path)
295 | DISTRIBUTIONPLIST += '\n' + ' '\
296 | '' % (plist_pkg_ref)
297 | DISTRIBUTIONPLIST += '\n' + ' '\
298 | ''
299 | DISTRIBUTIONPLIST += '\n' + ' '\
300 | '%s' % (
302 | plist_pkg_ref, plist_pkg_version,
303 | plist_pkg_relative_path)
304 | DISTRIBUTIONPLIST += '\n' + ''
305 |
306 | print 'Creating Distribution plist...'
307 |
308 | distribution_plist_path = os.path.join(temp_dir, 'dist.xml')
309 | with open(distribution_plist_path, 'wb') as f:
310 | f.write(DISTRIBUTIONPLIST)
311 |
312 | print 'Creating new distribution package...'
313 | pkg_created = create_distribution_package(
314 | distribution_plist_path, mountpoint, outputpath,
315 | plist_title)
316 |
317 | if pkg_created:
318 | print 'Package successfully created...'
319 | else:
320 | print 'Package build failed...'
321 |
322 | print 'Unmounting original dmg...'
323 | unmountdmg(mountpoint)
324 |
325 | print 'Cleaning up...'
326 | shutil.rmtree(temp_dir)
327 |
328 | else:
329 | print 'Could not obtain all of the keys from Xcode!'
330 | exit(1)
331 |
332 |
333 | if __name__ == '__main__':
334 | main()
335 |
--------------------------------------------------------------------------------