├── 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 | ![APInfo Slack Example](https://github.com/erikng/scripts/raw/master/APInfo/APInfo.png) 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 | --------------------------------------------------------------------------------