├── .gitattributes ├── .gitignore ├── BuildmacOSInstallApp.command ├── BuildmacOSInstallApp.py ├── LICENSE ├── MakeInstall.bat ├── MakeInstall.py ├── Readme.md ├── Scripts ├── BOOTICEx64.exe ├── __init__.py ├── disk.py ├── diskwin.py ├── downloader.py ├── plist.py ├── run.py └── utils.py ├── gibMacOS.bat ├── gibMacOS.command └── gibMacOS.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Ensure all .bat scripts use CRLF line endings 2 | # This can prevent a number of odd batch issues 3 | *.bat text eol=crlf 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Ignore hidden files 7 | .* 8 | 9 | # Ignore our save directory 10 | macOS Downloads 11 | 12 | # Ignore dd 13 | ddrelease64.exe 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | MANIFEST 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # celery beat schedule file 88 | celerybeat-schedule 89 | 90 | # SageMath parsed files 91 | *.sage.py 92 | 93 | # Environments 94 | .env 95 | .venv 96 | env/ 97 | venv/ 98 | ENV/ 99 | env.bak/ 100 | venv.bak/ 101 | 102 | # Spyder project settings 103 | .spyderproject 104 | .spyproject 105 | 106 | # Rope project settings 107 | .ropeproject 108 | 109 | # mkdocs documentation 110 | /site 111 | 112 | # mypy 113 | .mypy_cache/ 114 | -------------------------------------------------------------------------------- /BuildmacOSInstallApp.command: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the curent directory, the script name 4 | # and the script name with "py" substituted for the extension. 5 | args=( "$@" ) 6 | dir="$(cd -- "$(dirname "$0")" >/dev/null 2>&1; pwd -P)" 7 | script="${0##*/}" 8 | target="${script%.*}.py" 9 | 10 | # use_py3: 11 | # TRUE = Use if found, use py2 otherwise 12 | # FALSE = Use py2 13 | # FORCE = Use py3 14 | use_py3="TRUE" 15 | 16 | # We'll parse if the first argument passed is 17 | # --install-python and if so, we'll just install 18 | just_installing="FALSE" 19 | 20 | tempdir="" 21 | 22 | compare_to_version () { 23 | # Compares our OS version to the passed OS version, and 24 | # return a 1 if we match the passed compare type, or a 0 if we don't. 25 | # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal) 26 | # $2 = OS version to compare ours to 27 | if [ -z "$1" ] || [ -z "$2" ]; then 28 | # Missing info - bail. 29 | return 30 | fi 31 | local current_os= comp= 32 | current_os="$(sw_vers -productVersion)" 33 | comp="$(vercomp "$current_os" "$2")" 34 | # Check gequal and lequal first 35 | if [[ "$1" == "3" && ("$comp" == "1" || "$comp" == "0") ]] || [[ "$1" == "4" && ("$comp" == "2" || "$comp" == "0") ]] || [[ "$comp" == "$1" ]]; then 36 | # Matched 37 | echo "1" 38 | else 39 | # No match 40 | echo "0" 41 | fi 42 | } 43 | 44 | set_use_py3_if () { 45 | # Auto sets the "use_py3" variable based on 46 | # conditions passed 47 | # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal) 48 | # $2 = OS version to compare 49 | # $3 = TRUE/FALSE/FORCE in case of match 50 | if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then 51 | # Missing vars - bail with no changes. 52 | return 53 | fi 54 | if [ "$(compare_to_version "$1" "$2")" == "1" ]; then 55 | use_py3="$3" 56 | fi 57 | } 58 | 59 | get_remote_py_version () { 60 | local pyurl= py_html= py_vers= py_num="3" 61 | pyurl="https://www.python.org/downloads/macos/" 62 | py_html="$(curl -L $pyurl --compressed 2>&1)" 63 | if [ -z "$use_py3" ]; then 64 | use_py3="TRUE" 65 | fi 66 | if [ "$use_py3" == "FALSE" ]; then 67 | py_num="2" 68 | fi 69 | py_vers="$(echo "$py_html" | grep -i "Latest Python $py_num Release" | awk '{print $8}' | cut -d'<' -f1)" 70 | echo "$py_vers" 71 | } 72 | 73 | download_py () { 74 | local vers="$1" url= 75 | clear 76 | echo " ### ###" 77 | echo " # Downloading Python #" 78 | echo "### ###" 79 | echo 80 | if [ -z "$vers" ]; then 81 | echo "Gathering latest version..." 82 | vers="$(get_remote_py_version)" 83 | fi 84 | if [ -z "$vers" ]; then 85 | # Didn't get it still - bail 86 | print_error 87 | fi 88 | echo "Located Version: $vers" 89 | echo 90 | echo "Building download url..." 91 | url="$(curl -L https://www.python.org/downloads/release/python-${vers//./}/ --compressed 2>&1 | grep -iE "python-$vers-macos.*.pkg\"" | awk -F'"' '{ print $2 }')" 92 | if [ -z "$url" ]; then 93 | # Couldn't get the URL - bail 94 | print_error 95 | fi 96 | echo " - $url" 97 | echo 98 | echo "Downloading..." 99 | echo 100 | # Create a temp dir and download to it 101 | tempdir="$(mktemp -d 2>/dev/null || mktemp -d -t 'tempdir')" 102 | curl "$url" -o "$tempdir/python.pkg" 103 | if [ "$?" != "0" ]; then 104 | echo 105 | echo " - Failed to download python installer!" 106 | echo 107 | exit $? 108 | fi 109 | echo 110 | echo "Running python install package..." 111 | echo 112 | sudo installer -pkg "$tempdir/python.pkg" -target / 113 | if [ "$?" != "0" ]; then 114 | echo 115 | echo " - Failed to install python!" 116 | echo 117 | exit $? 118 | fi 119 | # Now we expand the package and look for a shell update script 120 | pkgutil --expand "$tempdir/python.pkg" "$tempdir/python" 121 | if [ -e "$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall" ]; then 122 | # Run the script 123 | echo 124 | echo "Updating PATH..." 125 | echo 126 | "$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall" 127 | fi 128 | vers_folder="Python $(echo "$vers" | cut -d'.' -f1 -f2)" 129 | if [ -f "/Applications/$vers_folder/Install Certificates.command" ]; then 130 | # Certs script exists - let's execute that to make sure our certificates are updated 131 | echo 132 | echo "Updating Certificates..." 133 | echo 134 | "/Applications/$vers_folder/Install Certificates.command" 135 | fi 136 | echo 137 | echo "Cleaning up..." 138 | cleanup 139 | echo 140 | if [ "$just_installing" == "TRUE" ]; then 141 | echo "Done." 142 | else 143 | # Now we check for py again 144 | echo "Rechecking py..." 145 | downloaded="TRUE" 146 | clear 147 | main 148 | fi 149 | } 150 | 151 | cleanup () { 152 | if [ -d "$tempdir" ]; then 153 | rm -Rf "$tempdir" 154 | fi 155 | } 156 | 157 | print_error() { 158 | clear 159 | cleanup 160 | echo " ### ###" 161 | echo " # Python Not Found #" 162 | echo "### ###" 163 | echo 164 | echo "Python is not installed or not found in your PATH var." 165 | echo 166 | if [ "$kernel" == "Darwin" ]; then 167 | echo "Please go to https://www.python.org/downloads/macos/ to" 168 | echo "download and install the latest version, then try again." 169 | else 170 | echo "Please install python through your package manager and" 171 | echo "try again." 172 | fi 173 | echo 174 | exit 1 175 | } 176 | 177 | print_target_missing() { 178 | clear 179 | cleanup 180 | echo " ### ###" 181 | echo " # Target Not Found #" 182 | echo "### ###" 183 | echo 184 | echo "Could not locate $target!" 185 | echo 186 | exit 1 187 | } 188 | 189 | format_version () { 190 | local vers="$1" 191 | echo "$(echo "$1" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }')" 192 | } 193 | 194 | vercomp () { 195 | # Modified from: https://apple.stackexchange.com/a/123408/11374 196 | local ver1="$(format_version "$1")" ver2="$(format_version "$2")" 197 | if [ $ver1 -gt $ver2 ]; then 198 | echo "1" 199 | elif [ $ver1 -lt $ver2 ]; then 200 | echo "2" 201 | else 202 | echo "0" 203 | fi 204 | } 205 | 206 | get_local_python_version() { 207 | # $1 = Python bin name (defaults to python3) 208 | # Echoes the path to the highest version of the passed python bin if any 209 | local py_name="$1" max_version= python= python_version= python_path= 210 | if [ -z "$py_name" ]; then 211 | py_name="python3" 212 | fi 213 | py_list="$(which -a "$py_name" 2>/dev/null)" 214 | # Walk that newline separated list 215 | while read python; do 216 | if [ -z "$python" ]; then 217 | # Got a blank line - skip 218 | continue 219 | fi 220 | if [ "$check_py3_stub" == "1" ] && [ "$python" == "/usr/bin/python3" ]; then 221 | # See if we have a valid developer path 222 | xcode-select -p > /dev/null 2>&1 223 | if [ "$?" != "0" ]; then 224 | # /usr/bin/python3 path - but no valid developer dir 225 | continue 226 | fi 227 | fi 228 | python_version="$(get_python_version $python)" 229 | if [ -z "$python_version" ]; then 230 | # Didn't find a py version - skip 231 | continue 232 | fi 233 | # Got the py version - compare to our max 234 | if [ -z "$max_version" ] || [ "$(vercomp "$python_version" "$max_version")" == "1" ]; then 235 | # Max not set, or less than the current - update it 236 | max_version="$python_version" 237 | python_path="$python" 238 | fi 239 | done <<< "$py_list" 240 | echo "$python_path" 241 | } 242 | 243 | get_python_version() { 244 | local py_path="$1" py_version= 245 | # Get the python version by piping stderr into stdout (for py2), then grepping the output for 246 | # the word "python", getting the second element, and grepping for an alphanumeric version number 247 | py_version="$($py_path -V 2>&1 | grep -i python | cut -d' ' -f2 | grep -E "[A-Za-z\d\.]+")" 248 | if [ ! -z "$py_version" ]; then 249 | echo "$py_version" 250 | fi 251 | } 252 | 253 | prompt_and_download() { 254 | if [ "$downloaded" != "FALSE" ] || [ "$kernel" != "Darwin" ]; then 255 | # We already tried to download, or we're not on macOS - just bail 256 | print_error 257 | fi 258 | clear 259 | echo " ### ###" 260 | echo " # Python Not Found #" 261 | echo "### ###" 262 | echo 263 | target_py="Python 3" 264 | printed_py="Python 2 or 3" 265 | if [ "$use_py3" == "FORCE" ]; then 266 | printed_py="Python 3" 267 | elif [ "$use_py3" == "FALSE" ]; then 268 | target_py="Python 2" 269 | printed_py="Python 2" 270 | fi 271 | echo "Could not locate $printed_py!" 272 | echo 273 | echo "This script requires $printed_py to run." 274 | echo 275 | while true; do 276 | read -p "Would you like to install the latest $target_py now? (y/n): " yn 277 | case $yn in 278 | [Yy]* ) download_py;break;; 279 | [Nn]* ) print_error;; 280 | esac 281 | done 282 | } 283 | 284 | main() { 285 | local python= version= 286 | # Verify our target exists 287 | if [ ! -f "$dir/$target" ]; then 288 | # Doesn't exist 289 | print_target_missing 290 | fi 291 | if [ -z "$use_py3" ]; then 292 | use_py3="TRUE" 293 | fi 294 | if [ "$use_py3" != "FALSE" ]; then 295 | # Check for py3 first 296 | python="$(get_local_python_version python3)" 297 | fi 298 | if [ "$use_py3" != "FORCE" ] && [ -z "$python" ]; then 299 | # We aren't using py3 explicitly, and we don't already have a path 300 | python="$(get_local_python_version python2)" 301 | if [ -z "$python" ]; then 302 | # Try just looking for "python" 303 | python="$(get_local_python_version python)" 304 | fi 305 | fi 306 | if [ -z "$python" ]; then 307 | # Didn't ever find it - prompt 308 | prompt_and_download 309 | return 1 310 | fi 311 | # Found it - start our script and pass all args 312 | "$python" "$dir/$target" "${args[@]}" 313 | } 314 | 315 | # Keep track of whether or not we're on macOS to determine if 316 | # we can download and install python for the user as needed. 317 | kernel="$(uname -s)" 318 | # Check to see if we need to force based on 319 | # macOS version. 10.15 has a dummy python3 version 320 | # that can trip up some py3 detection in other scripts. 321 | # set_use_py3_if "3" "10.15" "FORCE" 322 | downloaded="FALSE" 323 | # Check for the aforementioned /usr/bin/python3 stub if 324 | # our OS version is 10.15 or greater. 325 | check_py3_stub="$(compare_to_version "3" "10.15")" 326 | trap cleanup EXIT 327 | if [ "$1" == "--install-python" ] && [ "$kernel" == "Darwin" ]; then 328 | just_installing="TRUE" 329 | download_py 330 | else 331 | main 332 | fi 333 | -------------------------------------------------------------------------------- /BuildmacOSInstallApp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from Scripts import * 3 | import os, datetime, shutil, time, sys, argparse 4 | 5 | # Using the techniques outlined by wolfmannight here: https://www.insanelymac.com/forum/topic/338810-create-legit-copy-of-macos-from-apple-catalog/ 6 | 7 | class buildMacOSInstallApp: 8 | def __init__(self): 9 | self.r = run.Run() 10 | self.u = utils.Utils("Build macOS Install App") 11 | self.target_files = [ 12 | "BaseSystem.dmg", 13 | "BaseSystem.chunklist", 14 | "InstallESDDmg.pkg", 15 | "InstallInfo.plist", 16 | "AppleDiagnostics.dmg", 17 | "AppleDiagnostics.chunklist" 18 | ] 19 | # Verify we're on macOS - this doesn't work anywhere else 20 | if not sys.platform == "darwin": 21 | self.u.head("WARNING") 22 | print("") 23 | print("This script only runs on macOS!") 24 | print("") 25 | exit(1) 26 | 27 | def mount_dmg(self, dmg, no_browse = False): 28 | # Mounts the passed dmg and returns the mount point(s) 29 | args = ["/usr/bin/hdiutil", "attach", dmg, "-plist", "-noverify"] 30 | if no_browse: 31 | args.append("-nobrowse") 32 | out = self.r.run({"args":args}) 33 | if out[2] != 0: 34 | # Failed! 35 | raise Exception("Mount Failed!", "{} failed to mount:\n\n{}".format(os.path.basename(dmg), out[1])) 36 | # Get the plist data returned, and locate the mount points 37 | try: 38 | plist_data = plist.loads(out[0]) 39 | mounts = [x["mount-point"] for x in plist_data.get("system-entities", []) if "mount-point" in x] 40 | return mounts 41 | except: 42 | raise Exception("Mount Failed!", "No mount points returned from {}".format(os.path.basename(dmg))) 43 | 44 | def unmount_dmg(self, mount_point): 45 | # Unmounts the passed dmg or mount point - retries with force if failed 46 | # Can take either a single point or a list 47 | if not type(mount_point) is list: 48 | mount_point = [mount_point] 49 | unmounted = [] 50 | for m in mount_point: 51 | args = ["/usr/bin/hdiutil", "detach", m] 52 | out = self.r.run({"args":args}) 53 | if out[2] != 0: 54 | # Polite failed, let's crush this b! 55 | args.append("-force") 56 | out = self.r.run({"args":args}) 57 | if out[2] != 0: 58 | # Oh... failed again... onto the next... 59 | print(out[1]) 60 | continue 61 | unmounted.append(m) 62 | return unmounted 63 | 64 | def main(self): 65 | while True: 66 | self.u.head() 67 | print("") 68 | print("Q. Quit") 69 | print("") 70 | fold = self.u.grab("Please drag and drop the output folder from gibMacOS here: ") 71 | print("") 72 | if fold.lower() == "q": 73 | self.u.custom_quit() 74 | f_path = self.u.check_path(fold) 75 | if not f_path: 76 | print("That path does not exist!\n") 77 | self.u.grab("Press [enter] to return...") 78 | continue 79 | # Let's check if it's a folder. If not, make the next directory up the target 80 | if not os.path.isdir(f_path): 81 | f_path = os.path.dirname(os.path.realpath(f_path)) 82 | # Walk the contents of f_path and ensure we have all the needed files 83 | lower_contents = [y.lower() for y in os.listdir(f_path)] 84 | # Check if we got an InstallAssistant.pkg - and if so, just open that 85 | if "installassistant.pkg" in lower_contents: 86 | self.u.head("InstallAssistant.pkg Found") 87 | print("") 88 | print("Located InstallAssistant.pkg in the passed folder.\n") 89 | print("As of macOS Big Sur (11.x), Apple changed how they distribute the OS files in") 90 | print("the software update catalog.\n") 91 | print("Double clicking the InstallAssistant.pkg will open it in Installer, which will") 92 | print("copy the Install macOS [version].app to your /Applications folder.\n") 93 | print("Opening InstallAssistant.pkg...") 94 | self.r.run({"args":["open",os.path.join(f_path,"InstallAssistant.pkg")]}) 95 | print("") 96 | self.u.grab("Press [enter] to return...") 97 | continue 98 | missing_list = [x for x in self.target_files if not x.lower() in lower_contents] 99 | if len(missing_list): 100 | self.u.head("Missing Required Files") 101 | print("") 102 | print("That folder is missing the following required files:") 103 | print(", ".join(missing_list)) 104 | print("") 105 | self.u.grab("Press [enter] to return...") 106 | # Time to build the installer! 107 | cwd = os.getcwd() 108 | os.chdir(f_path) 109 | base_mounts = [] 110 | try: 111 | self.u.head("Building Installer") 112 | print("") 113 | print("Taking ownership of downloaded files...") 114 | for x in self.target_files: 115 | print(" - {}...".format(x)) 116 | self.r.run({"args":["chmod","a+x",x]}) 117 | print("Mounting BaseSystem.dmg...") 118 | base_mounts = self.mount_dmg("BaseSystem.dmg") 119 | if not len(base_mounts): 120 | raise Exception("Mount Failed!", "No mount points were returned from BaseSystem.dmg") 121 | base_mount = base_mounts[0] # Let's assume the first 122 | print("Locating Installer app...") 123 | install_app = next((x for x in os.listdir(base_mount) if os.path.isdir(os.path.join(base_mount,x)) and x.lower().endswith(".app") and not x.startswith(".")),None) 124 | if not install_app: 125 | raise Exception("Installer app not located in {}".format(base_mount)) 126 | print(" - Found {}".format(install_app)) 127 | # Copy the .app over 128 | out = self.r.run({"args":["cp","-R",os.path.join(base_mount,install_app),os.path.join(f_path,install_app)]}) 129 | if out[2] != 0: 130 | raise Exception("Copy Failed!", out[1]) 131 | print("Unmounting BaseSystem.dmg...") 132 | for x in base_mounts: 133 | self.unmount_dmg(x) 134 | base_mounts = [] 135 | shared_support = os.path.join(f_path,install_app,"Contents","SharedSupport") 136 | if not os.path.exists(shared_support): 137 | print("Creating SharedSupport directory...") 138 | os.makedirs(shared_support) 139 | print("Copying files to SharedSupport...") 140 | for x in self.target_files: 141 | y = "InstallESD.dmg" if x.lower() == "installesddmg.pkg" else x # InstallESDDmg.pkg gets renamed to InstallESD.dmg - all others stay the same 142 | print(" - {}{}".format(x, " --> {}".format(y) if y != x else "")) 143 | out = self.r.run({"args":["cp","-R",os.path.join(f_path,x),os.path.join(shared_support,y)]}) 144 | if out[2] != 0: 145 | raise Exception("Copy Failed!", out[1]) 146 | print("Patching InstallInfo.plist...") 147 | with open(os.path.join(shared_support,"InstallInfo.plist"),"rb") as f: 148 | p = plist.load(f) 149 | if "Payload Image Info" in p: 150 | pii = p["Payload Image Info"] 151 | if "URL" in pii: pii["URL"] = pii["URL"].replace("InstallESDDmg.pkg","InstallESD.dmg") 152 | if "id" in pii: pii["id"] = pii["id"].replace("com.apple.pkg.InstallESDDmg","com.apple.dmg.InstallESD") 153 | pii.pop("chunklistURL",None) 154 | pii.pop("chunklistid",None) 155 | with open(os.path.join(shared_support,"InstallInfo.plist"),"wb") as f: 156 | plist.dump(p,f) 157 | print("") 158 | print("Created: {}".format(install_app)) 159 | print("Saved to: {}".format(os.path.join(f_path,install_app))) 160 | print("") 161 | self.u.grab("Press [enter] to return...") 162 | except Exception as e: 163 | print("An error occurred:") 164 | print(" - {}".format(e)) 165 | print("") 166 | if len(base_mounts): 167 | for x in base_mounts: 168 | print(" - Unmounting {}...".format(x)) 169 | self.unmount_dmg(x) 170 | print("") 171 | self.u.grab("Press [enter] to return...") 172 | 173 | if __name__ == '__main__': 174 | b = buildMacOSInstallApp() 175 | b.main() 176 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 CorpNewt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MakeInstall.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM Get our local path before delayed expansion - allows ! in path 3 | set "thisDir=%~dp0" 4 | 5 | setlocal enableDelayedExpansion 6 | REM Setup initial vars 7 | set "script_name=" 8 | set /a tried=0 9 | set "toask=yes" 10 | set "pause_on_error=yes" 11 | set "py2v=" 12 | set "py2path=" 13 | set "py3v=" 14 | set "py3path=" 15 | set "pypath=" 16 | set "targetpy=3" 17 | 18 | REM use_py3: 19 | REM TRUE = Use if found, use py2 otherwise 20 | REM FALSE = Use py2 21 | REM FORCE = Use py3 22 | set "use_py3=TRUE" 23 | 24 | REM We'll parse if the first argument passed is 25 | REM --install-python and if so, we'll just install 26 | set "just_installing=FALSE" 27 | 28 | REM Get the system32 (or equivalent) path 29 | call :getsyspath "syspath" 30 | 31 | REM Make sure the syspath exists 32 | if "!syspath!" == "" ( 33 | if exist "%SYSTEMROOT%\system32\cmd.exe" ( 34 | if exist "%SYSTEMROOT%\system32\reg.exe" ( 35 | if exist "%SYSTEMROOT%\system32\where.exe" ( 36 | REM Fall back on the default path if it exists 37 | set "ComSpec=%SYSTEMROOT%\system32\cmd.exe" 38 | set "syspath=%SYSTEMROOT%\system32\" 39 | ) 40 | ) 41 | ) 42 | if "!syspath!" == "" ( 43 | cls 44 | echo ### ### 45 | echo # Warning # 46 | echo ### ### 47 | echo. 48 | echo Could not locate cmd.exe, reg.exe, or where.exe 49 | echo. 50 | echo Please ensure your ComSpec environment variable is properly configured and 51 | echo points directly to cmd.exe, then try again. 52 | echo. 53 | echo Current CompSpec Value: "%ComSpec%" 54 | echo. 55 | echo Press [enter] to quit. 56 | pause > nul 57 | exit /b 1 58 | ) 59 | ) 60 | 61 | if "%~1" == "--install-python" ( 62 | set "just_installing=TRUE" 63 | goto installpy 64 | ) 65 | 66 | goto checkscript 67 | 68 | :checkscript 69 | REM Check for our script first 70 | set "looking_for=!script_name!" 71 | if "!script_name!" == "" ( 72 | set "looking_for=%~n0.py or %~n0.command" 73 | set "script_name=%~n0.py" 74 | if not exist "!thisDir!\!script_name!" ( 75 | set "script_name=%~n0.command" 76 | ) 77 | ) 78 | if not exist "!thisDir!\!script_name!" ( 79 | echo Could not find !looking_for!. 80 | echo Please make sure to run this script from the same directory 81 | echo as !looking_for!. 82 | echo. 83 | echo Press [enter] to quit. 84 | pause > nul 85 | exit /b 1 86 | ) 87 | goto checkpy 88 | 89 | :checkpy 90 | call :updatepath 91 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe python 2^> nul`) do ( call :checkpyversion "%%x" "py2v" "py2path" "py3v" "py3path" ) 92 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe python3 2^> nul`) do ( call :checkpyversion "%%x" "py2v" "py2path" "py3v" "py3path" ) 93 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe py 2^> nul`) do ( call :checkpylauncher "%%x" "py2v" "py2path" "py3v" "py3path" ) 94 | REM Walk our returns to see if we need to install 95 | if /i "!use_py3!" == "FALSE" ( 96 | set "targetpy=2" 97 | set "pypath=!py2path!" 98 | ) else if /i "!use_py3!" == "FORCE" ( 99 | set "pypath=!py3path!" 100 | ) else if /i "!use_py3!" == "TRUE" ( 101 | set "pypath=!py3path!" 102 | if "!pypath!" == "" set "pypath=!py2path!" 103 | ) 104 | if not "!pypath!" == "" ( 105 | goto runscript 106 | ) 107 | if !tried! lss 1 ( 108 | if /i "!toask!"=="yes" ( 109 | REM Better ask permission first 110 | goto askinstall 111 | ) else ( 112 | goto installpy 113 | ) 114 | ) else ( 115 | cls 116 | echo ### ### 117 | echo # Warning # 118 | echo ### ### 119 | echo. 120 | REM Couldn't install for whatever reason - give the error message 121 | echo Python is not installed or not found in your PATH var. 122 | echo Please install it from https://www.python.org/downloads/windows/ 123 | echo. 124 | echo Make sure you check the box labeled: 125 | echo. 126 | echo "Add Python X.X to PATH" 127 | echo. 128 | echo Where X.X is the py version you're installing. 129 | echo. 130 | echo Press [enter] to quit. 131 | pause > nul 132 | exit /b 1 133 | ) 134 | goto runscript 135 | 136 | :checkpylauncher 137 | REM Attempt to check the latest python 2 and 3 versions via the py launcher 138 | for /f "USEBACKQ tokens=*" %%x in (`%~1 -2 -c "import sys; print(sys.executable)" 2^> nul`) do ( call :checkpyversion "%%x" "%~2" "%~3" "%~4" "%~5" ) 139 | for /f "USEBACKQ tokens=*" %%x in (`%~1 -3 -c "import sys; print(sys.executable)" 2^> nul`) do ( call :checkpyversion "%%x" "%~2" "%~3" "%~4" "%~5" ) 140 | goto :EOF 141 | 142 | :checkpyversion 143 | set "version="&for /f "tokens=2* USEBACKQ delims= " %%a in (`"%~1" -V 2^>^&1`) do ( 144 | REM Ensure we have a version number 145 | call :isnumber "%%a" 146 | if not "!errorlevel!" == "0" goto :EOF 147 | set "version=%%a" 148 | ) 149 | if not defined version goto :EOF 150 | if "!version:~0,1!" == "2" ( 151 | REM Python 2 152 | call :comparepyversion "!version!" "!%~2!" 153 | if "!errorlevel!" == "1" ( 154 | set "%~2=!version!" 155 | set "%~3=%~1" 156 | ) 157 | ) else ( 158 | REM Python 3 159 | call :comparepyversion "!version!" "!%~4!" 160 | if "!errorlevel!" == "1" ( 161 | set "%~4=!version!" 162 | set "%~5=%~1" 163 | ) 164 | ) 165 | goto :EOF 166 | 167 | :isnumber 168 | set "var="&for /f "delims=0123456789." %%i in ("%~1") do set var=%%i 169 | if defined var (exit /b 1) 170 | exit /b 0 171 | 172 | :comparepyversion 173 | REM Exits with status 0 if equal, 1 if v1 gtr v2, 2 if v1 lss v2 174 | for /f "tokens=1,2,3 delims=." %%a in ("%~1") do ( 175 | set a1=%%a 176 | set a2=%%b 177 | set a3=%%c 178 | ) 179 | for /f "tokens=1,2,3 delims=." %%a in ("%~2") do ( 180 | set b1=%%a 181 | set b2=%%b 182 | set b3=%%c 183 | ) 184 | if not defined a1 set a1=0 185 | if not defined a2 set a2=0 186 | if not defined a3 set a3=0 187 | if not defined b1 set b1=0 188 | if not defined b2 set b2=0 189 | if not defined b3 set b3=0 190 | if %a1% gtr %b1% exit /b 1 191 | if %a1% lss %b1% exit /b 2 192 | if %a2% gtr %b2% exit /b 1 193 | if %a2% lss %b2% exit /b 2 194 | if %a3% gtr %b3% exit /b 1 195 | if %a3% lss %b3% exit /b 2 196 | exit /b 0 197 | 198 | :askinstall 199 | cls 200 | echo ### ### 201 | echo # Python Not Found # 202 | echo ### ### 203 | echo. 204 | echo Python !targetpy! was not found on the system or in the PATH var. 205 | echo. 206 | set /p "menu=Would you like to install it now? [y/n]: " 207 | if /i "!menu!"=="y" ( 208 | REM We got the OK - install it 209 | goto installpy 210 | ) else if "!menu!"=="n" ( 211 | REM No OK here... 212 | set /a tried=!tried!+1 213 | goto checkpy 214 | ) 215 | REM Incorrect answer - go back 216 | goto askinstall 217 | 218 | :installpy 219 | REM This will attempt to download and install python 220 | REM First we get the html for the python downloads page for Windows 221 | set /a tried=!tried!+1 222 | cls 223 | echo ### ### 224 | echo # Installing Python # 225 | echo ### ### 226 | echo. 227 | echo Gathering info from https://www.python.org/downloads/windows/... 228 | powershell -command "[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12;(new-object System.Net.WebClient).DownloadFile('https://www.python.org/downloads/windows/','%TEMP%\pyurl.txt')" 229 | REM Extract it if it's gzip compressed 230 | powershell -command "$infile='%TEMP%\pyurl.txt';$outfile='%TEMP%\pyurl.temp';try{$input=New-Object System.IO.FileStream $infile,([IO.FileMode]::Open),([IO.FileAccess]::Read),([IO.FileShare]::Read);$output=New-Object System.IO.FileStream $outfile,([IO.FileMode]::Create),([IO.FileAccess]::Write),([IO.FileShare]::None);$gzipStream=New-Object System.IO.Compression.GzipStream $input,([IO.Compression.CompressionMode]::Decompress);$buffer=New-Object byte[](1024);while($true){$read=$gzipstream.Read($buffer,0,1024);if($read -le 0){break};$output.Write($buffer,0,$read)};$gzipStream.Close();$output.Close();$input.Close();Move-Item -Path $outfile -Destination $infile -Force}catch{}" 231 | if not exist "%TEMP%\pyurl.txt" ( 232 | if /i "!just_installing!" == "TRUE" ( 233 | echo Failed to get info 234 | exit /b 1 235 | ) else ( 236 | goto checkpy 237 | ) 238 | ) 239 | echo Parsing for latest... 240 | pushd "%TEMP%" 241 | :: Version detection code slimmed by LussacZheng (https://github.com/corpnewt/gibMacOS/issues/20) 242 | for /f "tokens=9 delims=< " %%x in ('findstr /i /c:"Latest Python !targetpy! Release" pyurl.txt') do ( set "release=%%x" ) 243 | popd 244 | if "!release!" == "" ( 245 | if /i "!just_installing!" == "TRUE" ( 246 | echo Failed to get python version 247 | exit /b 1 248 | ) else ( 249 | goto checkpy 250 | ) 251 | ) 252 | echo Found Python !release! - Downloading... 253 | REM Let's delete our txt file now - we no longer need it 254 | del "%TEMP%\pyurl.txt" 255 | REM At this point - we should have the version number. 256 | REM We can build the url like so: "https://www.python.org/ftp/python/[version]/python-[version]-amd64.exe" 257 | set "url=https://www.python.org/ftp/python/!release!/python-!release!-amd64.exe" 258 | set "pytype=exe" 259 | if "!targetpy!" == "2" ( 260 | set "url=https://www.python.org/ftp/python/!release!/python-!release!.amd64.msi" 261 | set "pytype=msi" 262 | ) 263 | REM Now we download it with our slick powershell command 264 | powershell -command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (new-object System.Net.WebClient).DownloadFile('!url!','%TEMP%\pyinstall.!pytype!')" 265 | REM If it doesn't exist - we bail 266 | if not exist "%TEMP%\pyinstall.!pytype!" ( 267 | if /i "!just_installing!" == "TRUE" ( 268 | echo Failed to download installer 269 | exit /b 1 270 | ) else ( 271 | goto checkpy 272 | ) 273 | ) 274 | REM It should exist at this point - let's run it to install silently 275 | echo Installing... 276 | pushd "%TEMP%" 277 | if /i "!pytype!" == "exe" ( 278 | echo pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 279 | pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 280 | ) else ( 281 | set "foldername=!release:.=!" 282 | echo msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR="%LocalAppData%\Programs\Python\Python!foldername:~0,2!" 283 | msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR="%LocalAppData%\Programs\Python\Python!foldername:~0,2!" 284 | ) 285 | popd 286 | echo Installer finished with %ERRORLEVEL% status. 287 | REM Now we should be able to delete the installer and check for py again 288 | del "%TEMP%\pyinstall.!pytype!" 289 | REM If it worked, then we should have python in our PATH 290 | REM this does not get updated right away though - let's try 291 | REM manually updating the local PATH var 292 | call :updatepath 293 | if /i "!just_installing!" == "TRUE" ( 294 | echo. 295 | echo Done. 296 | ) else ( 297 | goto checkpy 298 | ) 299 | exit /b 300 | 301 | :runscript 302 | REM Python found 303 | cls 304 | set "args=%*" 305 | set "args=!args:"=!" 306 | if "!args!"=="" ( 307 | "!pypath!" "!thisDir!!script_name!" 308 | ) else ( 309 | "!pypath!" "!thisDir!!script_name!" %* 310 | ) 311 | if /i "!pause_on_error!" == "yes" ( 312 | if not "%ERRORLEVEL%" == "0" ( 313 | echo. 314 | echo Script exited with error code: %ERRORLEVEL% 315 | echo. 316 | echo Press [enter] to exit... 317 | pause > nul 318 | ) 319 | ) 320 | goto :EOF 321 | 322 | :undouble 323 | REM Helper function to strip doubles of a single character out of a string recursively 324 | set "string_value=%~2" 325 | :undouble_continue 326 | set "check=!string_value:%~3%~3=%~3!" 327 | if not "!check!" == "!string_value!" ( 328 | set "string_value=!check!" 329 | goto :undouble_continue 330 | ) 331 | set "%~1=!check!" 332 | goto :EOF 333 | 334 | :updatepath 335 | set "spath=" 336 | set "upath=" 337 | for /f "USEBACKQ tokens=2* delims= " %%i in (`!syspath!reg.exe query "HKCU\Environment" /v "Path" 2^> nul`) do ( if not "%%j" == "" set "upath=%%j" ) 338 | for /f "USEBACKQ tokens=2* delims= " %%i in (`!syspath!reg.exe query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "Path" 2^> nul`) do ( if not "%%j" == "" set "spath=%%j" ) 339 | if not "%spath%" == "" ( 340 | REM We got something in the system path 341 | set "PATH=%spath%" 342 | if not "%upath%" == "" ( 343 | REM We also have something in the user path 344 | set "PATH=%PATH%;%upath%" 345 | ) 346 | ) else if not "%upath%" == "" ( 347 | set "PATH=%upath%" 348 | ) 349 | REM Remove double semicolons from the adjusted PATH 350 | call :undouble "PATH" "%PATH%" ";" 351 | goto :EOF 352 | 353 | :getsyspath 354 | REM Helper method to return a valid path to cmd.exe, reg.exe, and where.exe by 355 | REM walking the ComSpec var - will also repair it in memory if need be 356 | REM Strip double semi-colons 357 | call :undouble "temppath" "%ComSpec%" ";" 358 | 359 | REM Dirty hack to leverage the "line feed" approach - there are some odd side 360 | REM effects with this. Do not use this variable name in comments near this 361 | REM line - as it seems to behave erradically. 362 | (set LF=^ 363 | %=this line is empty=% 364 | ) 365 | REM Replace instances of semi-colons with a line feed and wrap 366 | REM in parenthesis to work around some strange batch behavior 367 | set "testpath=%temppath:;=!LF!%" 368 | 369 | REM Let's walk each path and test if cmd.exe, reg.exe, and where.exe exist there 370 | set /a found=0 371 | for /f "tokens=* delims=" %%i in ("!testpath!") do ( 372 | REM Only continue if we haven't found it yet 373 | if not "%%i" == "" ( 374 | if !found! lss 1 ( 375 | set "checkpath=%%i" 376 | REM Remove "cmd.exe" from the end if it exists 377 | if /i "!checkpath:~-7!" == "cmd.exe" ( 378 | set "checkpath=!checkpath:~0,-7!" 379 | ) 380 | REM Pad the end with a backslash if needed 381 | if not "!checkpath:~-1!" == "\" ( 382 | set "checkpath=!checkpath!\" 383 | ) 384 | REM Let's see if cmd, reg, and where exist there - and set it if so 385 | if EXIST "!checkpath!cmd.exe" ( 386 | if EXIST "!checkpath!reg.exe" ( 387 | if EXIST "!checkpath!where.exe" ( 388 | set /a found=1 389 | set "ComSpec=!checkpath!cmd.exe" 390 | set "%~1=!checkpath!" 391 | ) 392 | ) 393 | ) 394 | ) 395 | ) 396 | ) 397 | goto :EOF 398 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | Py2/py3 script that can download macOS components direct from Apple 2 | 3 | Can also now build Internet Recovery USB installers from Windows using [dd](http://www.chrysocome.net/dd) and [7zip](https://www.7-zip.org/download.html). 4 | 5 | **NOTE:** As of macOS 11 (Big Sur), Apple has changed the way they distribute macOS, and internet recovery USBs can no longer be built via MakeInstall on Windows. macOS versions through Catalina will still work though. 6 | 7 | **NOTE 2:** As of macOS 11 (Big Sur), Apple distributes the OS via an InstallAssistant.pkg file. `BuildmacOSInstallApp.command` is not needed to create the install application when in macOS in this case - and you can simply run `InstallAssistant.pkg`, which will place the install app in your /Applications folder on macOS. 8 | 9 | Thanks to: 10 | 11 | * FoxletFox for [FetchMacOS](http://www.insanelymac.com/forum/topic/326366-fetchmacos-a-tool-to-download-macos-on-non-mac-platforms/) and outlining the URL setup 12 | * munki for his [macadmin-scripts](https://github.com/munki/macadmin-scripts) 13 | * timsutton for [brigadier](https://github.com/timsutton/brigadier) 14 | * wolfmannight for [manOSDownloader_rc](https://www.insanelymac.com/forum/topic/338810-create-legit-copy-of-macos-from-apple-catalog/) off which BuildmacOSInstallApp.command is based 15 | -------------------------------------------------------------------------------- /Scripts/BOOTICEx64.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corpnewt/gibMacOS/25c411355f7a3abc54d3cdd1e766cc43e0103064/Scripts/BOOTICEx64.exe -------------------------------------------------------------------------------- /Scripts/__init__.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname, basename, isfile 2 | import glob 3 | modules = glob.glob(dirname(__file__)+"/*.py") 4 | __all__ = [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')] -------------------------------------------------------------------------------- /Scripts/disk.py: -------------------------------------------------------------------------------- 1 | import subprocess, plistlib, sys, os, time, json 2 | sys.path.append(os.path.abspath(os.path.dirname(os.path.realpath(__file__)))) 3 | import run 4 | if sys.version_info < (3,0): 5 | # Force use of StringIO instead of cStringIO as the latter 6 | # has issues with Unicode strings 7 | from StringIO import StringIO 8 | 9 | class Disk: 10 | 11 | def __init__(self): 12 | self.r = run.Run() 13 | self.diskutil = self.get_diskutil() 14 | self.os_version = ".".join( 15 | self.r.run({"args":["sw_vers", "-productVersion"]})[0].split(".")[:2] 16 | ) 17 | self.full_os_version = self.r.run({"args":["sw_vers", "-productVersion"]})[0] 18 | if len(self.full_os_version.split(".")) < 3: 19 | # Add .0 in case of 10.14 20 | self.full_os_version += ".0" 21 | self.sudo_mount_version = "10.13.6" 22 | self.sudo_mount_types = ["efi"] 23 | self.apfs = {} 24 | self._update_disks() 25 | 26 | def _get_str(self, val): 27 | # Helper method to return a string value based on input type 28 | if (sys.version_info < (3,0) and isinstance(val, unicode)) or (sys.version_info >= (3,0) and isinstance(val, bytes)): 29 | return val.encode("utf-8") 30 | return str(val) 31 | 32 | def _get_plist(self, s): 33 | p = {} 34 | try: 35 | if sys.version_info >= (3, 0): 36 | p = plistlib.loads(s.encode("utf-8")) 37 | else: 38 | # p = plistlib.readPlistFromString(s) 39 | # We avoid using readPlistFromString() as that uses 40 | # cStringIO and fails when Unicode strings are detected 41 | # Don't subclass - keep the parser local 42 | from xml.parsers.expat import ParserCreate 43 | # Create a new PlistParser object - then we need to set up 44 | # the values and parse. 45 | pa = plistlib.PlistParser() 46 | # We also monkey patch this to encode unicode as utf-8 47 | def end_string(): 48 | d = pa.getData() 49 | if isinstance(d,unicode): 50 | d = d.encode("utf-8") 51 | pa.addObject(d) 52 | pa.end_string = end_string 53 | parser = ParserCreate() 54 | parser.StartElementHandler = pa.handleBeginElement 55 | parser.EndElementHandler = pa.handleEndElement 56 | parser.CharacterDataHandler = pa.handleData 57 | if isinstance(s, unicode): 58 | # Encode unicode -> string; use utf-8 for safety 59 | s = s.encode("utf-8") 60 | # Parse the string 61 | parser.Parse(s, 1) 62 | p = pa.root 63 | except Exception as e: 64 | print(e) 65 | pass 66 | return p 67 | 68 | def _compare_versions(self, vers1, vers2, pad = -1): 69 | # Helper method to compare ##.## strings 70 | # 71 | # vers1 < vers2 = True 72 | # vers1 = vers2 = None 73 | # vers1 > vers2 = False 74 | # 75 | # Must be separated with a period 76 | 77 | # Sanitize the pads 78 | pad = -1 if not type(pad) is int else pad 79 | 80 | # Cast as strings 81 | vers1 = str(vers1) 82 | vers2 = str(vers2) 83 | 84 | # Split to lists 85 | v1_parts = vers1.split(".") 86 | v2_parts = vers2.split(".") 87 | 88 | # Equalize lengths 89 | if len(v1_parts) < len(v2_parts): 90 | v1_parts.extend([str(pad) for x in range(len(v2_parts) - len(v1_parts))]) 91 | elif len(v2_parts) < len(v1_parts): 92 | v2_parts.extend([str(pad) for x in range(len(v1_parts) - len(v2_parts))]) 93 | 94 | # Iterate and compare 95 | for i in range(len(v1_parts)): 96 | # Remove non-numeric 97 | v1 = ''.join(c for c in v1_parts[i] if c.isdigit()) 98 | v2 = ''.join(c for c in v2_parts[i] if c.isdigit()) 99 | # If empty - make it a pad var 100 | v1 = pad if not len(v1) else v1 101 | v2 = pad if not len(v2) else v2 102 | # Compare 103 | if int(v1) < int(v2): 104 | return True 105 | elif int(v1) > int(v2): 106 | return False 107 | # Never differed - return None, must be equal 108 | return None 109 | 110 | def update(self): 111 | self._update_disks() 112 | 113 | def _update_disks(self): 114 | self.disks = self.get_disks() 115 | self.disk_text = self.get_disk_text() 116 | if self._compare_versions("10.12", self.os_version): 117 | self.apfs = self.get_apfs() 118 | else: 119 | self.apfs = {} 120 | 121 | def get_diskutil(self): 122 | # Returns the path to the diskutil binary 123 | return self.r.run({"args":["which", "diskutil"]})[0].split("\n")[0].split("\r")[0] 124 | 125 | def get_disks(self): 126 | # Returns a dictionary object of connected disks 127 | disk_list = self.r.run({"args":[self.diskutil, "list", "-plist"]})[0] 128 | return self._get_plist(disk_list) 129 | 130 | def get_disk_text(self): 131 | # Returns plain text listing connected disks 132 | return self.r.run({"args":[self.diskutil, "list"]})[0] 133 | 134 | def get_disk_info(self, disk): 135 | disk_id = self.get_identifier(disk) 136 | if not disk_id: 137 | return None 138 | disk_list = self.r.run({"args":[self.diskutil, "info", "-plist", disk_id]})[0] 139 | return self._get_plist(disk_list) 140 | 141 | def get_disk_fs(self, disk): 142 | disk_id = self.get_identifier(disk) 143 | if not disk_id: 144 | return None 145 | return self.get_disk_info(disk_id).get("FilesystemName", None) 146 | 147 | def get_disk_fs_type(self, disk): 148 | disk_id = self.get_identifier(disk) 149 | if not disk_id: 150 | return None 151 | return self.get_disk_info(disk_id).get("FilesystemType", None) 152 | 153 | def get_apfs(self): 154 | # Returns a dictionary object of apfs disks 155 | output = self.r.run({"args":"echo y | " + self.diskutil + " apfs list -plist", "shell" : True}) 156 | if not output[2] == 0: 157 | # Error getting apfs info - return an empty dict 158 | return {} 159 | disk_list = output[0] 160 | p_list = disk_list.split(" 1: 162 | # We had text before the start - get only the plist info 163 | disk_list = "= 1), "B") 38 | # Determine our rounding approach - first make sure it's an int; default to 2 on error 39 | try:round_to=int(round_to) 40 | except:round_to=2 41 | round_to = 0 if round_to < 0 else 15 if round_to > 15 else round_to # Ensure it's between 0 and 15 42 | bval = round(s_dict[biggest], round_to) 43 | # Split our number based on decimal points 44 | a,b = str(bval).split(".") 45 | # Check if we need to strip or pad zeroes 46 | b = b.rstrip("0") if strip_zeroes else b.ljust(round_to,"0") if round_to > 0 else "" 47 | return "{:,}{} {}".format(int(a),"" if not b else "."+b,biggest) 48 | 49 | def _process_hook(queue, total_size, bytes_so_far=0, update_interval=1.0, max_packets=0): 50 | packets = [] 51 | speed = remaining = "" 52 | last_update = time.time() 53 | while True: 54 | # Write our info first so we have *some* status while 55 | # waiting for packets 56 | if total_size > 0: 57 | percent = float(bytes_so_far) / total_size 58 | percent = round(percent*100, 2) 59 | t_s = get_size(total_size) 60 | try: 61 | b_s = get_size(bytes_so_far, t_s.split(" ")[1]) 62 | except: 63 | b_s = get_size(bytes_so_far) 64 | perc_str = " {:.2f}%".format(percent) 65 | bar_width = (TERMINAL_WIDTH // 3)-len(perc_str) 66 | progress = "=" * int(bar_width * (percent/100)) 67 | sys.stdout.write("\r\033[K{}/{} | {}{}{}{}{}".format( 68 | b_s, 69 | t_s, 70 | progress, 71 | " " * (bar_width-len(progress)), 72 | perc_str, 73 | speed, 74 | remaining 75 | )) 76 | else: 77 | b_s = get_size(bytes_so_far) 78 | sys.stdout.write("\r\033[K{}{}".format(b_s, speed)) 79 | sys.stdout.flush() 80 | # Now we gather the next packet 81 | try: 82 | packet = queue.get(timeout=update_interval) 83 | # Packets should be formatted as a tuple of 84 | # (timestamp, len(bytes_downloaded)) 85 | # If "DONE" is passed, we assume the download 86 | # finished - and bail 87 | if packet == "DONE": 88 | print("") # Jump to the next line 89 | return 90 | # Append our packet to the list and ensure we're not 91 | # beyond our max. 92 | # Only check max if it's > 0 93 | packets.append(packet) 94 | if max_packets > 0: 95 | packets = packets[-max_packets:] 96 | # Increment our bytes so far as well 97 | bytes_so_far += packet[1] 98 | except q.Empty: 99 | # Didn't get anything - reset the speed 100 | # and packets 101 | packets = [] 102 | speed = " | 0 B/s" 103 | remaining = " | ?? left" if total_size > 0 else "" 104 | except KeyboardInterrupt: 105 | print("") # Jump to the next line 106 | return 107 | # If we have packets and it's time for an update, process 108 | # the info. 109 | update_check = time.time() 110 | if packets and update_check - last_update >= update_interval: 111 | last_update = update_check # Refresh our update timestamp 112 | speed = " | ?? B/s" 113 | if len(packets) > 1: 114 | # Let's calculate the amount downloaded over how long 115 | try: 116 | first,last = packets[0][0],packets[-1][0] 117 | chunks = sum([float(x[1]) for x in packets]) 118 | t = last-first 119 | assert t >= 0 120 | bytes_speed = 1. / t * chunks 121 | speed = " | {}/s".format(get_size(bytes_speed,round_to=1)) 122 | # Get our remaining time 123 | if total_size > 0: 124 | seconds_left = (total_size-bytes_so_far) / bytes_speed 125 | days = seconds_left // 86400 126 | hours = (seconds_left - (days*86400)) // 3600 127 | mins = (seconds_left - (days*86400) - (hours*3600)) // 60 128 | secs = seconds_left - (days*86400) - (hours*3600) - (mins*60) 129 | if days > 99 or bytes_speed == 0: 130 | remaining = " | ?? left" 131 | else: 132 | remaining = " | {}{:02d}:{:02d}:{:02d} left".format( 133 | "{}:".format(int(days)) if days else "", 134 | int(hours), 135 | int(mins), 136 | int(round(secs)) 137 | ) 138 | except: 139 | pass 140 | # Clear the packets so we don't reuse the same ones 141 | packets = [] 142 | 143 | class Downloader: 144 | 145 | def __init__(self,**kwargs): 146 | self.ua = kwargs.get("useragent",{"User-Agent":"Mozilla"}) 147 | self.chunk = 1048576 # 1024 x 1024 i.e. 1MiB 148 | if os.name=="nt": os.system("color") # Initialize cmd for ANSI escapes 149 | # Provide reasonable default logic to workaround macOS CA file handling 150 | cafile = ssl.get_default_verify_paths().openssl_cafile 151 | try: 152 | # If default OpenSSL CA file does not exist, use that from certifi 153 | if not os.path.exists(cafile): 154 | import certifi 155 | cafile = certifi.where() 156 | self.ssl_context = ssl.create_default_context(cafile=cafile) 157 | except: 158 | # None of the above worked, disable certificate verification for now 159 | self.ssl_context = ssl._create_unverified_context() 160 | return 161 | 162 | def _decode(self, value, encoding="utf-8", errors="ignore"): 163 | # Helper method to only decode if bytes type 164 | if sys.version_info >= (3,0) and isinstance(value, bytes): 165 | return value.decode(encoding,errors) 166 | return value 167 | 168 | def _update_main_name(self): 169 | # Windows running python 2 seems to have issues with multiprocessing 170 | # if the case of the main script's name is incorrect: 171 | # e.g. Downloader.py vs downloader.py 172 | # 173 | # To work around this, we try to scrape for the correct case if 174 | # possible. 175 | try: 176 | path = os.path.abspath(sys.modules["__main__"].__file__) 177 | except AttributeError as e: 178 | # This likely means we're running from the interpreter 179 | # directly 180 | return None 181 | if not os.path.isfile(path): 182 | return None 183 | # Get the file name and folder path 184 | name = os.path.basename(path).lower() 185 | fldr = os.path.dirname(path) 186 | # Walk the files in the folder until we find our 187 | # name - then steal its case and update that path 188 | for f in os.listdir(fldr): 189 | if f.lower() == name: 190 | # Got it 191 | new_path = os.path.join(fldr,f) 192 | sys.modules["__main__"].__file__ = new_path 193 | return new_path 194 | # If we got here, it wasn't found 195 | return None 196 | 197 | def _get_headers(self, headers = None): 198 | # Fall back on the default ua if none provided 199 | target = headers if isinstance(headers,dict) else self.ua 200 | new_headers = {} 201 | # Shallow copy to prevent changes to the headers 202 | # overriding the original 203 | for k in target: 204 | new_headers[k] = target[k] 205 | return new_headers 206 | 207 | def open_url(self, url, headers = None): 208 | headers = self._get_headers(headers) 209 | # Wrap up the try/except block so we don't have to do this for each function 210 | try: 211 | response = urlopen(Request(url, headers=headers), context=self.ssl_context) 212 | except Exception as e: 213 | # No fixing this - bail 214 | return None 215 | return response 216 | 217 | def get_size(self, *args, **kwargs): 218 | return get_size(*args,**kwargs) 219 | 220 | def get_string(self, url, progress = True, headers = None, expand_gzip = True): 221 | response = self.get_bytes(url,progress,headers,expand_gzip) 222 | if response is None: return None 223 | return self._decode(response) 224 | 225 | def get_bytes(self, url, progress = True, headers = None, expand_gzip = True): 226 | response = self.open_url(url, headers) 227 | if response is None: return None 228 | try: total_size = int(response.headers['Content-Length']) 229 | except: total_size = -1 230 | chunk_so_far = b"" 231 | packets = queue = process = None 232 | if progress: 233 | # Make sure our vars are initialized 234 | packets = [] if progress else None 235 | queue = multiprocessing.Queue() 236 | # Create the multiprocess and start it 237 | process = multiprocessing.Process( 238 | target=_process_hook, 239 | args=(queue,total_size) 240 | ) 241 | process.daemon = True 242 | # Filthy hack for earlier python versions on Windows 243 | if os.name == "nt" and hasattr(multiprocessing,"forking"): 244 | self._update_main_name() 245 | process.start() 246 | try: 247 | while True: 248 | chunk = response.read(self.chunk) 249 | if progress: 250 | # Add our items to the queue 251 | queue.put((time.time(),len(chunk))) 252 | if not chunk: break 253 | chunk_so_far += chunk 254 | finally: 255 | # Close the response whenever we're done 256 | response.close() 257 | if expand_gzip and response.headers.get("Content-Encoding","unknown").lower() == "gzip": 258 | fileobj = BytesIO(chunk_so_far) 259 | gfile = gzip.GzipFile(fileobj=fileobj) 260 | return gfile.read() 261 | if progress: 262 | # Finalize the queue and wait 263 | queue.put("DONE") 264 | process.join() 265 | return chunk_so_far 266 | 267 | def stream_to_file(self, url, file_path, progress = True, headers = None, ensure_size_if_present = True, allow_resume = False): 268 | response = self.open_url(url, headers) 269 | if response is None: return None 270 | bytes_so_far = 0 271 | try: total_size = int(response.headers['Content-Length']) 272 | except: total_size = -1 273 | packets = queue = process = None 274 | mode = "wb" 275 | if allow_resume and os.path.isfile(file_path) and total_size != -1: 276 | # File exists, we're resuming and have a target size. Check the 277 | # local file size. 278 | current_size = os.stat(file_path).st_size 279 | if current_size == total_size: 280 | # File is already complete - return the path 281 | return file_path 282 | elif current_size < total_size: 283 | response.close() 284 | # File is not complete - seek to our current size 285 | bytes_so_far = current_size 286 | mode = "ab" # Append 287 | # We also need to try creating a new request 288 | # in order to pass our range header 289 | new_headers = self._get_headers(headers) 290 | # Get the start byte, 0-indexed 291 | byte_string = "bytes={}-".format(current_size) 292 | new_headers["Range"] = byte_string 293 | response = self.open_url(url, new_headers) 294 | if response is None: return None 295 | if progress: 296 | # Make sure our vars are initialized 297 | packets = [] if progress else None 298 | queue = multiprocessing.Queue() 299 | # Create the multiprocess and start it 300 | process = multiprocessing.Process( 301 | target=_process_hook, 302 | args=(queue,total_size,bytes_so_far) 303 | ) 304 | process.daemon = True 305 | # Filthy hack for earlier python versions on Windows 306 | if os.name == "nt" and hasattr(multiprocessing,"forking"): 307 | self._update_main_name() 308 | process.start() 309 | with open(file_path,mode) as f: 310 | try: 311 | while True: 312 | chunk = response.read(self.chunk) 313 | bytes_so_far += len(chunk) 314 | if progress: 315 | # Add our items to the queue 316 | queue.put((time.time(),len(chunk))) 317 | if not chunk: break 318 | f.write(chunk) 319 | finally: 320 | # Close the response whenever we're done 321 | response.close() 322 | if progress: 323 | # Finalize the queue and wait 324 | queue.put("DONE") 325 | process.join() 326 | if ensure_size_if_present and total_size != -1: 327 | # We're verifying size - make sure we got what we asked for 328 | if bytes_so_far != total_size: 329 | return None # We didn't - imply it failed 330 | return file_path if os.path.exists(file_path) else None 331 | -------------------------------------------------------------------------------- /Scripts/plist.py: -------------------------------------------------------------------------------- 1 | ### ### 2 | # Imports # 3 | ### ### 4 | 5 | import datetime, os, plistlib, struct, sys, itertools, binascii 6 | from io import BytesIO 7 | 8 | if sys.version_info < (3,0): 9 | # Force use of StringIO instead of cStringIO as the latter 10 | # has issues with Unicode strings 11 | from StringIO import StringIO 12 | else: 13 | from io import StringIO 14 | 15 | try: 16 | basestring # Python 2 17 | unicode 18 | except NameError: 19 | basestring = str # Python 3 20 | unicode = str 21 | 22 | try: 23 | FMT_XML = plistlib.FMT_XML 24 | FMT_BINARY = plistlib.FMT_BINARY 25 | except AttributeError: 26 | FMT_XML = "FMT_XML" 27 | FMT_BINARY = "FMT_BINARY" 28 | 29 | ### ### 30 | # Helper Methods # 31 | ### ### 32 | 33 | def wrap_data(value): 34 | if not _check_py3(): return plistlib.Data(value) 35 | return value 36 | 37 | def extract_data(value): 38 | if not _check_py3() and isinstance(value,plistlib.Data): return value.data 39 | return value 40 | 41 | def _check_py3(): 42 | return sys.version_info >= (3, 0) 43 | 44 | def _is_binary(fp): 45 | if isinstance(fp, basestring): 46 | return fp.startswith(b"bplist00") 47 | header = fp.read(32) 48 | fp.seek(0) 49 | return header[:8] == b'bplist00' 50 | 51 | ### ### 52 | # Deprecated Functions - Remapped # 53 | ### ### 54 | 55 | def readPlist(pathOrFile): 56 | if not isinstance(pathOrFile, basestring): 57 | return load(pathOrFile) 58 | with open(pathOrFile, "rb") as f: 59 | return load(f) 60 | 61 | def writePlist(value, pathOrFile): 62 | if not isinstance(pathOrFile, basestring): 63 | return dump(value, pathOrFile, fmt=FMT_XML, sort_keys=True, skipkeys=False) 64 | with open(pathOrFile, "wb") as f: 65 | return dump(value, f, fmt=FMT_XML, sort_keys=True, skipkeys=False) 66 | 67 | ### ### 68 | # Remapped Functions # 69 | ### ### 70 | 71 | def load(fp, fmt=None, use_builtin_types=None, dict_type=dict): 72 | if _is_binary(fp): 73 | use_builtin_types = False if use_builtin_types is None else use_builtin_types 74 | try: 75 | p = _BinaryPlistParser(use_builtin_types=use_builtin_types, dict_type=dict_type) 76 | except: 77 | # Python 3.9 removed use_builtin_types 78 | p = _BinaryPlistParser(dict_type=dict_type) 79 | return p.parse(fp) 80 | elif _check_py3(): 81 | use_builtin_types = True if use_builtin_types is None else use_builtin_types 82 | # We need to monkey patch this to allow for hex integers - code taken/modified from 83 | # https://github.com/python/cpython/blob/3.8/Lib/plistlib.py 84 | if fmt is None: 85 | header = fp.read(32) 86 | fp.seek(0) 87 | for info in plistlib._FORMATS.values(): 88 | if info['detect'](header): 89 | P = info['parser'] 90 | break 91 | else: 92 | raise plistlib.InvalidFileException() 93 | else: 94 | P = plistlib._FORMATS[fmt]['parser'] 95 | try: 96 | p = P(use_builtin_types=use_builtin_types, dict_type=dict_type) 97 | except: 98 | # Python 3.9 removed use_builtin_types 99 | p = P(dict_type=dict_type) 100 | if isinstance(p,plistlib._PlistParser): 101 | # Monkey patch! 102 | def end_integer(): 103 | d = p.get_data() 104 | value = int(d,16) if d.lower().startswith("0x") else int(d) 105 | if -1 << 63 <= value < 1 << 64: 106 | p.add_object(value) 107 | else: 108 | raise OverflowError("Integer overflow at line {}".format(p.parser.CurrentLineNumber)) 109 | def end_data(): 110 | try: 111 | p.add_object(plistlib._decode_base64(p.get_data())) 112 | except Exception as e: 113 | raise Exception("Data error at line {}: {}".format(p.parser.CurrentLineNumber,e)) 114 | p.end_integer = end_integer 115 | p.end_data = end_data 116 | return p.parse(fp) 117 | else: 118 | # Is not binary - assume a string - and try to load 119 | # We avoid using readPlistFromString() as that uses 120 | # cStringIO and fails when Unicode strings are detected 121 | # Don't subclass - keep the parser local 122 | from xml.parsers.expat import ParserCreate 123 | # Create a new PlistParser object - then we need to set up 124 | # the values and parse. 125 | p = plistlib.PlistParser() 126 | parser = ParserCreate() 127 | parser.StartElementHandler = p.handleBeginElement 128 | parser.EndElementHandler = p.handleEndElement 129 | parser.CharacterDataHandler = p.handleData 130 | # We also need to monkey patch this to allow for other dict_types, hex int support 131 | # proper line output for data errors, and for unicode string decoding 132 | def begin_dict(attrs): 133 | d = dict_type() 134 | p.addObject(d) 135 | p.stack.append(d) 136 | def end_integer(): 137 | d = p.getData() 138 | value = int(d,16) if d.lower().startswith("0x") else int(d) 139 | if -1 << 63 <= value < 1 << 64: 140 | p.addObject(value) 141 | else: 142 | raise OverflowError("Integer overflow at line {}".format(parser.CurrentLineNumber)) 143 | def end_data(): 144 | try: 145 | p.addObject(plistlib.Data.fromBase64(p.getData())) 146 | except Exception as e: 147 | raise Exception("Data error at line {}: {}".format(parser.CurrentLineNumber,e)) 148 | def end_string(): 149 | d = p.getData() 150 | if isinstance(d,unicode): 151 | d = d.encode("utf-8") 152 | p.addObject(d) 153 | p.begin_dict = begin_dict 154 | p.end_integer = end_integer 155 | p.end_data = end_data 156 | p.end_string = end_string 157 | if isinstance(fp, unicode): 158 | # Encode unicode -> string; use utf-8 for safety 159 | fp = fp.encode("utf-8") 160 | if isinstance(fp, basestring): 161 | # It's a string - let's wrap it up 162 | fp = StringIO(fp) 163 | # Parse it 164 | parser.ParseFile(fp) 165 | return p.root 166 | 167 | def loads(value, fmt=None, use_builtin_types=None, dict_type=dict): 168 | if _check_py3() and isinstance(value, basestring): 169 | # If it's a string - encode it 170 | value = value.encode() 171 | try: 172 | return load(BytesIO(value),fmt=fmt,use_builtin_types=use_builtin_types,dict_type=dict_type) 173 | except: 174 | # Python 3.9 removed use_builtin_types 175 | return load(BytesIO(value),fmt=fmt,dict_type=dict_type) 176 | 177 | def dump(value, fp, fmt=FMT_XML, sort_keys=True, skipkeys=False): 178 | if fmt == FMT_BINARY: 179 | # Assume binary at this point 180 | writer = _BinaryPlistWriter(fp, sort_keys=sort_keys, skipkeys=skipkeys) 181 | writer.write(value) 182 | elif fmt == FMT_XML: 183 | if _check_py3(): 184 | plistlib.dump(value, fp, fmt=fmt, sort_keys=sort_keys, skipkeys=skipkeys) 185 | else: 186 | # We need to monkey patch a bunch here too in order to avoid auto-sorting 187 | # of keys 188 | writer = plistlib.PlistWriter(fp) 189 | def writeDict(d): 190 | if d: 191 | writer.beginElement("dict") 192 | items = sorted(d.items()) if sort_keys else d.items() 193 | for key, value in items: 194 | if not isinstance(key, basestring): 195 | if skipkeys: 196 | continue 197 | raise TypeError("keys must be strings") 198 | writer.simpleElement("key", key) 199 | writer.writeValue(value) 200 | writer.endElement("dict") 201 | else: 202 | writer.simpleElement("dict") 203 | writer.writeDict = writeDict 204 | writer.writeln("") 205 | writer.writeValue(value) 206 | writer.writeln("") 207 | else: 208 | # Not a proper format 209 | raise ValueError("Unsupported format: {}".format(fmt)) 210 | 211 | def dumps(value, fmt=FMT_XML, skipkeys=False, sort_keys=True): 212 | # We avoid using writePlistToString() as that uses 213 | # cStringIO and fails when Unicode strings are detected 214 | f = BytesIO() if _check_py3() else StringIO() 215 | dump(value, f, fmt=fmt, skipkeys=skipkeys, sort_keys=sort_keys) 216 | value = f.getvalue() 217 | if _check_py3(): 218 | value = value.decode("utf-8") 219 | return value 220 | 221 | ### ### 222 | # Binary Plist Stuff For Py2 # 223 | ### ### 224 | 225 | # From the python 3 plistlib.py source: https://github.com/python/cpython/blob/3.11/Lib/plistlib.py 226 | # Tweaked to function on both Python 2 and 3 227 | 228 | class UID: 229 | def __init__(self, data): 230 | if not isinstance(data, int): 231 | raise TypeError("data must be an int") 232 | # It seems Apple only uses 32-bit unsigned ints for UIDs. Although the comment in 233 | # CoreFoundation's CFBinaryPList.c detailing the binary plist format theoretically 234 | # allows for 64-bit UIDs, most functions in the same file use 32-bit unsigned ints, 235 | # with the sole function hinting at 64-bits appearing to be a leftover from copying 236 | # and pasting integer handling code internally, and this code has not changed since 237 | # it was added. (In addition, code in CFPropertyList.c to handle CF$UID also uses a 238 | # 32-bit unsigned int.) 239 | # 240 | # if data >= 1 << 64: 241 | # raise ValueError("UIDs cannot be >= 2**64") 242 | if data >= 1 << 32: 243 | raise ValueError("UIDs cannot be >= 2**32 (4294967296)") 244 | if data < 0: 245 | raise ValueError("UIDs must be positive") 246 | self.data = data 247 | 248 | def __index__(self): 249 | return self.data 250 | 251 | def __repr__(self): 252 | return "%s(%s)" % (self.__class__.__name__, repr(self.data)) 253 | 254 | def __reduce__(self): 255 | return self.__class__, (self.data,) 256 | 257 | def __eq__(self, other): 258 | if not isinstance(other, UID): 259 | return NotImplemented 260 | return self.data == other.data 261 | 262 | def __hash__(self): 263 | return hash(self.data) 264 | 265 | class InvalidFileException (ValueError): 266 | def __init__(self, message="Invalid file"): 267 | ValueError.__init__(self, message) 268 | 269 | _BINARY_FORMAT = {1: 'B', 2: 'H', 4: 'L', 8: 'Q'} 270 | 271 | _undefined = object() 272 | 273 | class _BinaryPlistParser: 274 | """ 275 | Read or write a binary plist file, following the description of the binary 276 | format. Raise InvalidFileException in case of error, otherwise return the 277 | root object. 278 | see also: http://opensource.apple.com/source/CF/CF-744.18/CFBinaryPList.c 279 | """ 280 | def __init__(self, use_builtin_types, dict_type): 281 | self._use_builtin_types = use_builtin_types 282 | self._dict_type = dict_type 283 | 284 | def parse(self, fp): 285 | try: 286 | # The basic file format: 287 | # HEADER 288 | # object... 289 | # refid->offset... 290 | # TRAILER 291 | self._fp = fp 292 | self._fp.seek(-32, os.SEEK_END) 293 | trailer = self._fp.read(32) 294 | if len(trailer) != 32: 295 | raise InvalidFileException() 296 | ( 297 | offset_size, self._ref_size, num_objects, top_object, 298 | offset_table_offset 299 | ) = struct.unpack('>6xBBQQQ', trailer) 300 | self._fp.seek(offset_table_offset) 301 | self._object_offsets = self._read_ints(num_objects, offset_size) 302 | self._objects = [_undefined] * num_objects 303 | return self._read_object(top_object) 304 | 305 | except (OSError, IndexError, struct.error, OverflowError, 306 | UnicodeDecodeError): 307 | raise InvalidFileException() 308 | 309 | def _get_size(self, tokenL): 310 | """ return the size of the next object.""" 311 | if tokenL == 0xF: 312 | m = self._fp.read(1)[0] 313 | if not _check_py3(): 314 | m = ord(m) 315 | m = m & 0x3 316 | s = 1 << m 317 | f = '>' + _BINARY_FORMAT[s] 318 | return struct.unpack(f, self._fp.read(s))[0] 319 | 320 | return tokenL 321 | 322 | def _read_ints(self, n, size): 323 | data = self._fp.read(size * n) 324 | if size in _BINARY_FORMAT: 325 | return struct.unpack('>' + _BINARY_FORMAT[size] * n, data) 326 | else: 327 | if not size or len(data) != size * n: 328 | raise InvalidFileException() 329 | return tuple(int(binascii.hexlify(data[i: i + size]),16) 330 | for i in range(0, size * n, size)) 331 | '''return tuple(int.from_bytes(data[i: i + size], 'big') 332 | for i in range(0, size * n, size))''' 333 | 334 | def _read_refs(self, n): 335 | return self._read_ints(n, self._ref_size) 336 | 337 | def _read_object(self, ref): 338 | """ 339 | read the object by reference. 340 | May recursively read sub-objects (content of an array/dict/set) 341 | """ 342 | result = self._objects[ref] 343 | if result is not _undefined: 344 | return result 345 | 346 | offset = self._object_offsets[ref] 347 | self._fp.seek(offset) 348 | token = self._fp.read(1)[0] 349 | if not _check_py3(): 350 | token = ord(token) 351 | tokenH, tokenL = token & 0xF0, token & 0x0F 352 | 353 | if token == 0x00: # \x00 or 0x00 354 | result = None 355 | 356 | elif token == 0x08: # \x08 or 0x08 357 | result = False 358 | 359 | elif token == 0x09: # \x09 or 0x09 360 | result = True 361 | 362 | # The referenced source code also mentions URL (0x0c, 0x0d) and 363 | # UUID (0x0e), but neither can be generated using the Cocoa libraries. 364 | 365 | elif token == 0x0f: # \x0f or 0x0f 366 | result = b'' 367 | 368 | elif tokenH == 0x10: # int 369 | result = int(binascii.hexlify(self._fp.read(1 << tokenL)),16) 370 | if tokenL >= 3: # Signed - adjust 371 | result = result-((result & 0x8000000000000000) << 1) 372 | 373 | elif token == 0x22: # real 374 | result = struct.unpack('>f', self._fp.read(4))[0] 375 | 376 | elif token == 0x23: # real 377 | result = struct.unpack('>d', self._fp.read(8))[0] 378 | 379 | elif token == 0x33: # date 380 | f = struct.unpack('>d', self._fp.read(8))[0] 381 | # timestamp 0 of binary plists corresponds to 1/1/2001 382 | # (year of Mac OS X 10.0), instead of 1/1/1970. 383 | result = (datetime.datetime(2001, 1, 1) + 384 | datetime.timedelta(seconds=f)) 385 | 386 | elif tokenH == 0x40: # data 387 | s = self._get_size(tokenL) 388 | if self._use_builtin_types or not hasattr(plistlib, "Data"): 389 | result = self._fp.read(s) 390 | else: 391 | result = plistlib.Data(self._fp.read(s)) 392 | 393 | elif tokenH == 0x50: # ascii string 394 | s = self._get_size(tokenL) 395 | result = self._fp.read(s).decode('ascii') 396 | result = result 397 | 398 | elif tokenH == 0x60: # unicode string 399 | s = self._get_size(tokenL) 400 | result = self._fp.read(s * 2).decode('utf-16be') 401 | 402 | elif tokenH == 0x80: # UID 403 | # used by Key-Archiver plist files 404 | result = UID(int(binascii.hexlify(self._fp.read(1 + tokenL)),16)) 405 | 406 | elif tokenH == 0xA0: # array 407 | s = self._get_size(tokenL) 408 | obj_refs = self._read_refs(s) 409 | result = [] 410 | self._objects[ref] = result 411 | result.extend(self._read_object(x) for x in obj_refs) 412 | 413 | # tokenH == 0xB0 is documented as 'ordset', but is not actually 414 | # implemented in the Apple reference code. 415 | 416 | # tokenH == 0xC0 is documented as 'set', but sets cannot be used in 417 | # plists. 418 | 419 | elif tokenH == 0xD0: # dict 420 | s = self._get_size(tokenL) 421 | key_refs = self._read_refs(s) 422 | obj_refs = self._read_refs(s) 423 | result = self._dict_type() 424 | self._objects[ref] = result 425 | for k, o in zip(key_refs, obj_refs): 426 | key = self._read_object(k) 427 | if hasattr(plistlib, "Data") and isinstance(key, plistlib.Data): 428 | key = key.data 429 | result[key] = self._read_object(o) 430 | 431 | else: 432 | raise InvalidFileException() 433 | 434 | self._objects[ref] = result 435 | return result 436 | 437 | def _count_to_size(count): 438 | if count < 1 << 8: 439 | return 1 440 | 441 | elif count < 1 << 16: 442 | return 2 443 | 444 | elif count < 1 << 32: 445 | return 4 446 | 447 | else: 448 | return 8 449 | 450 | _scalars = (str, int, float, datetime.datetime, bytes) 451 | 452 | class _BinaryPlistWriter (object): 453 | def __init__(self, fp, sort_keys, skipkeys): 454 | self._fp = fp 455 | self._sort_keys = sort_keys 456 | self._skipkeys = skipkeys 457 | 458 | def write(self, value): 459 | 460 | # Flattened object list: 461 | self._objlist = [] 462 | 463 | # Mappings from object->objectid 464 | # First dict has (type(object), object) as the key, 465 | # second dict is used when object is not hashable and 466 | # has id(object) as the key. 467 | self._objtable = {} 468 | self._objidtable = {} 469 | 470 | # Create list of all objects in the plist 471 | self._flatten(value) 472 | 473 | # Size of object references in serialized containers 474 | # depends on the number of objects in the plist. 475 | num_objects = len(self._objlist) 476 | self._object_offsets = [0]*num_objects 477 | self._ref_size = _count_to_size(num_objects) 478 | 479 | self._ref_format = _BINARY_FORMAT[self._ref_size] 480 | 481 | # Write file header 482 | self._fp.write(b'bplist00') 483 | 484 | # Write object list 485 | for obj in self._objlist: 486 | self._write_object(obj) 487 | 488 | # Write refnum->object offset table 489 | top_object = self._getrefnum(value) 490 | offset_table_offset = self._fp.tell() 491 | offset_size = _count_to_size(offset_table_offset) 492 | offset_format = '>' + _BINARY_FORMAT[offset_size] * num_objects 493 | self._fp.write(struct.pack(offset_format, *self._object_offsets)) 494 | 495 | # Write trailer 496 | sort_version = 0 497 | trailer = ( 498 | sort_version, offset_size, self._ref_size, num_objects, 499 | top_object, offset_table_offset 500 | ) 501 | self._fp.write(struct.pack('>5xBBBQQQ', *trailer)) 502 | 503 | def _flatten(self, value): 504 | # First check if the object is in the object table, not used for 505 | # containers to ensure that two subcontainers with the same contents 506 | # will be serialized as distinct values. 507 | if isinstance(value, _scalars): 508 | if (type(value), value) in self._objtable: 509 | return 510 | 511 | elif hasattr(plistlib, "Data") and isinstance(value, plistlib.Data): 512 | if (type(value.data), value.data) in self._objtable: 513 | return 514 | 515 | elif id(value) in self._objidtable: 516 | return 517 | 518 | # Add to objectreference map 519 | refnum = len(self._objlist) 520 | self._objlist.append(value) 521 | if isinstance(value, _scalars): 522 | self._objtable[(type(value), value)] = refnum 523 | elif hasattr(plistlib, "Data") and isinstance(value, plistlib.Data): 524 | self._objtable[(type(value.data), value.data)] = refnum 525 | else: 526 | self._objidtable[id(value)] = refnum 527 | 528 | # And finally recurse into containers 529 | if isinstance(value, dict): 530 | keys = [] 531 | values = [] 532 | items = value.items() 533 | if self._sort_keys: 534 | items = sorted(items) 535 | 536 | for k, v in items: 537 | if not isinstance(k, basestring): 538 | if self._skipkeys: 539 | continue 540 | raise TypeError("keys must be strings") 541 | keys.append(k) 542 | values.append(v) 543 | 544 | for o in itertools.chain(keys, values): 545 | self._flatten(o) 546 | 547 | elif isinstance(value, (list, tuple)): 548 | for o in value: 549 | self._flatten(o) 550 | 551 | def _getrefnum(self, value): 552 | if isinstance(value, _scalars): 553 | return self._objtable[(type(value), value)] 554 | elif hasattr(plistlib, "Data") and isinstance(value, plistlib.Data): 555 | return self._objtable[(type(value.data), value.data)] 556 | else: 557 | return self._objidtable[id(value)] 558 | 559 | def _write_size(self, token, size): 560 | if size < 15: 561 | self._fp.write(struct.pack('>B', token | size)) 562 | 563 | elif size < 1 << 8: 564 | self._fp.write(struct.pack('>BBB', token | 0xF, 0x10, size)) 565 | 566 | elif size < 1 << 16: 567 | self._fp.write(struct.pack('>BBH', token | 0xF, 0x11, size)) 568 | 569 | elif size < 1 << 32: 570 | self._fp.write(struct.pack('>BBL', token | 0xF, 0x12, size)) 571 | 572 | else: 573 | self._fp.write(struct.pack('>BBQ', token | 0xF, 0x13, size)) 574 | 575 | def _write_object(self, value): 576 | ref = self._getrefnum(value) 577 | self._object_offsets[ref] = self._fp.tell() 578 | if value is None: 579 | self._fp.write(b'\x00') 580 | 581 | elif value is False: 582 | self._fp.write(b'\x08') 583 | 584 | elif value is True: 585 | self._fp.write(b'\x09') 586 | 587 | elif isinstance(value, int): 588 | if value < 0: 589 | try: 590 | self._fp.write(struct.pack('>Bq', 0x13, value)) 591 | except struct.error: 592 | raise OverflowError(value) # from None 593 | elif value < 1 << 8: 594 | self._fp.write(struct.pack('>BB', 0x10, value)) 595 | elif value < 1 << 16: 596 | self._fp.write(struct.pack('>BH', 0x11, value)) 597 | elif value < 1 << 32: 598 | self._fp.write(struct.pack('>BL', 0x12, value)) 599 | elif value < 1 << 63: 600 | self._fp.write(struct.pack('>BQ', 0x13, value)) 601 | elif value < 1 << 64: 602 | self._fp.write(b'\x14' + value.to_bytes(16, 'big', signed=True)) 603 | else: 604 | raise OverflowError(value) 605 | 606 | elif isinstance(value, float): 607 | self._fp.write(struct.pack('>Bd', 0x23, value)) 608 | 609 | elif isinstance(value, datetime.datetime): 610 | f = (value - datetime.datetime(2001, 1, 1)).total_seconds() 611 | self._fp.write(struct.pack('>Bd', 0x33, f)) 612 | 613 | elif (_check_py3() and isinstance(value, (bytes, bytearray))) or (hasattr(plistlib, "Data") and isinstance(value, plistlib.Data)): 614 | if not isinstance(value, (bytes, bytearray)): 615 | value = value.data # Unpack it 616 | self._write_size(0x40, len(value)) 617 | self._fp.write(value) 618 | 619 | elif isinstance(value, basestring): 620 | try: 621 | t = value.encode('ascii') 622 | self._write_size(0x50, len(value)) 623 | except UnicodeEncodeError: 624 | t = value.encode('utf-16be') 625 | self._write_size(0x60, len(t) // 2) 626 | self._fp.write(t) 627 | 628 | elif isinstance(value, UID) or (hasattr(plistlib,"UID") and isinstance(value, plistlib.UID)): 629 | if value.data < 0: 630 | raise ValueError("UIDs must be positive") 631 | elif value.data < 1 << 8: 632 | self._fp.write(struct.pack('>BB', 0x80, value)) 633 | elif value.data < 1 << 16: 634 | self._fp.write(struct.pack('>BH', 0x81, value)) 635 | elif value.data < 1 << 32: 636 | self._fp.write(struct.pack('>BL', 0x83, value)) 637 | # elif value.data < 1 << 64: 638 | # self._fp.write(struct.pack('>BQ', 0x87, value)) 639 | else: 640 | raise OverflowError(value) 641 | 642 | elif isinstance(value, (list, tuple)): 643 | refs = [self._getrefnum(o) for o in value] 644 | s = len(refs) 645 | self._write_size(0xA0, s) 646 | self._fp.write(struct.pack('>' + self._ref_format * s, *refs)) 647 | 648 | elif isinstance(value, dict): 649 | keyRefs, valRefs = [], [] 650 | 651 | if self._sort_keys: 652 | rootItems = sorted(value.items()) 653 | else: 654 | rootItems = value.items() 655 | 656 | for k, v in rootItems: 657 | if not isinstance(k, basestring): 658 | if self._skipkeys: 659 | continue 660 | raise TypeError("keys must be strings") 661 | keyRefs.append(self._getrefnum(k)) 662 | valRefs.append(self._getrefnum(v)) 663 | 664 | s = len(keyRefs) 665 | self._write_size(0xD0, s) 666 | self._fp.write(struct.pack('>' + self._ref_format * s, *keyRefs)) 667 | self._fp.write(struct.pack('>' + self._ref_format * s, *valRefs)) 668 | 669 | else: 670 | raise TypeError(value) 671 | -------------------------------------------------------------------------------- /Scripts/run.py: -------------------------------------------------------------------------------- 1 | import sys, subprocess, time, threading, shlex 2 | try: 3 | from Queue import Queue, Empty 4 | except: 5 | from queue import Queue, Empty 6 | 7 | ON_POSIX = 'posix' in sys.builtin_module_names 8 | 9 | class Run: 10 | 11 | def __init__(self): 12 | return 13 | 14 | def _read_output(self, pipe, q): 15 | try: 16 | for line in iter(lambda: pipe.read(1), b''): 17 | q.put(line) 18 | except ValueError: 19 | pass 20 | pipe.close() 21 | 22 | def _create_thread(self, output): 23 | # Creates a new queue and thread object to watch based on the output pipe sent 24 | q = Queue() 25 | t = threading.Thread(target=self._read_output, args=(output, q)) 26 | t.daemon = True 27 | return (q,t) 28 | 29 | def _stream_output(self, comm, shell = False): 30 | output = error = "" 31 | p = None 32 | try: 33 | if shell and type(comm) is list: 34 | comm = " ".join(shlex.quote(x) for x in comm) 35 | if not shell and type(comm) is str: 36 | comm = shlex.split(comm) 37 | p = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0, universal_newlines=True, close_fds=ON_POSIX) 38 | # Setup the stdout thread/queue 39 | q,t = self._create_thread(p.stdout) 40 | qe,te = self._create_thread(p.stderr) 41 | # Start both threads 42 | t.start() 43 | te.start() 44 | 45 | while True: 46 | c = z = "" 47 | try: c = q.get_nowait() 48 | except Empty: pass 49 | else: 50 | sys.stdout.write(c) 51 | output += c 52 | sys.stdout.flush() 53 | try: z = qe.get_nowait() 54 | except Empty: pass 55 | else: 56 | sys.stderr.write(z) 57 | error += z 58 | sys.stderr.flush() 59 | if not c==z=="": continue # Keep going until empty 60 | # No output - see if still running 61 | p.poll() 62 | if p.returncode != None: 63 | # Subprocess ended 64 | break 65 | # No output, but subprocess still running - stall for 20ms 66 | time.sleep(0.02) 67 | 68 | o, e = p.communicate() 69 | return (output+o, error+e, p.returncode) 70 | except: 71 | if p: 72 | try: o, e = p.communicate() 73 | except: o = e = "" 74 | return (output+o, error+e, p.returncode) 75 | return ("", "Command not found!", 1) 76 | 77 | def _decode(self, value, encoding="utf-8", errors="ignore"): 78 | # Helper method to only decode if bytes type 79 | if sys.version_info >= (3,0) and isinstance(value, bytes): 80 | return value.decode(encoding,errors) 81 | return value 82 | 83 | def _run_command(self, comm, shell = False): 84 | c = None 85 | try: 86 | if shell and type(comm) is list: 87 | comm = " ".join(shlex.quote(x) for x in comm) 88 | if not shell and type(comm) is str: 89 | comm = shlex.split(comm) 90 | p = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 91 | c = p.communicate() 92 | except: 93 | if c == None: 94 | return ("", "Command not found!", 1) 95 | return (self._decode(c[0]), self._decode(c[1]), p.returncode) 96 | 97 | def run(self, command_list, leave_on_fail = False): 98 | # Command list should be an array of dicts 99 | if type(command_list) is dict: 100 | # We only have one command 101 | command_list = [command_list] 102 | output_list = [] 103 | for comm in command_list: 104 | args = comm.get("args", []) 105 | shell = comm.get("shell", False) 106 | stream = comm.get("stream", False) 107 | sudo = comm.get("sudo", False) 108 | stdout = comm.get("stdout", False) 109 | stderr = comm.get("stderr", False) 110 | mess = comm.get("message", None) 111 | show = comm.get("show", False) 112 | 113 | if not mess == None: 114 | print(mess) 115 | 116 | if not len(args): 117 | # nothing to process 118 | continue 119 | if sudo: 120 | # Check if we have sudo 121 | out = self._run_command(["which", "sudo"]) 122 | if "sudo" in out[0]: 123 | # Can sudo 124 | if type(args) is list: 125 | args.insert(0, out[0].replace("\n", "")) # add to start of list 126 | elif type(args) is str: 127 | args = out[0].replace("\n", "") + " " + args # add to start of string 128 | 129 | if show: 130 | print(" ".join(args)) 131 | 132 | if stream: 133 | # Stream it! 134 | out = self._stream_output(args, shell) 135 | else: 136 | # Just run and gather output 137 | out = self._run_command(args, shell) 138 | if stdout and len(out[0]): 139 | print(out[0]) 140 | if stderr and len(out[1]): 141 | print(out[1]) 142 | # Append output 143 | output_list.append(out) 144 | # Check for errors 145 | if leave_on_fail and out[2] != 0: 146 | # Got an error - leave 147 | break 148 | if len(output_list) == 1: 149 | # We only ran one command - just return that output 150 | return output_list[0] 151 | return output_list 152 | -------------------------------------------------------------------------------- /Scripts/utils.py: -------------------------------------------------------------------------------- 1 | import sys, os, time, re, json, datetime, ctypes, subprocess 2 | 3 | if os.name == "nt": 4 | # Windows 5 | import msvcrt 6 | else: 7 | # Not Windows \o/ 8 | import select 9 | 10 | class Utils: 11 | 12 | def __init__(self, name = "Python Script", interactive = True): 13 | self.name = name 14 | self.interactive = interactive 15 | # Init our colors before we need to print anything 16 | cwd = os.getcwd() 17 | os.chdir(os.path.dirname(os.path.realpath(__file__))) 18 | if os.path.exists("colors.json"): 19 | self.colors_dict = json.load(open("colors.json")) 20 | else: 21 | self.colors_dict = {} 22 | os.chdir(cwd) 23 | 24 | def check_admin(self): 25 | # Returns whether or not we're admin 26 | try: 27 | is_admin = os.getuid() == 0 28 | except AttributeError: 29 | is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0 30 | return is_admin 31 | 32 | def elevate(self, file): 33 | # Runs the passed file as admin 34 | if self.check_admin(): 35 | return 36 | if os.name == "nt": 37 | ctypes.windll.shell32.ShellExecuteW(None, "runas", '"{}"'.format(sys.executable), '"{}"'.format(file), None, 1) 38 | else: 39 | try: 40 | p = subprocess.Popen(["which", "sudo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 41 | c = p.communicate()[0].decode("utf-8", "ignore").replace("\n", "") 42 | os.execv(c, [ sys.executable, 'python'] + sys.argv) 43 | except: 44 | exit(1) 45 | 46 | def compare_versions(self, vers1, vers2, **kwargs): 47 | # Helper method to compare ##.## strings 48 | # 49 | # vers1 < vers2 = True 50 | # vers1 = vers2 = None 51 | # vers1 > vers2 = False 52 | 53 | # Sanitize the pads 54 | pad = str(kwargs.get("pad", "")) 55 | sep = str(kwargs.get("separator", ".")) 56 | 57 | ignore_case = kwargs.get("ignore_case", True) 58 | 59 | # Cast as strings 60 | vers1 = str(vers1) 61 | vers2 = str(vers2) 62 | 63 | if ignore_case: 64 | vers1 = vers1.lower() 65 | vers2 = vers2.lower() 66 | 67 | # Split and pad lists 68 | v1_parts, v2_parts = self.pad_length(vers1.split(sep), vers2.split(sep)) 69 | 70 | # Iterate and compare 71 | for i in range(len(v1_parts)): 72 | # Remove non-numeric 73 | v1 = ''.join(c.lower() for c in v1_parts[i] if c.isalnum()) 74 | v2 = ''.join(c.lower() for c in v2_parts[i] if c.isalnum()) 75 | # Equalize the lengths 76 | v1, v2 = self.pad_length(v1, v2) 77 | # Compare 78 | if str(v1) < str(v2): 79 | return True 80 | elif str(v1) > str(v2): 81 | return False 82 | # Never differed - return None, must be equal 83 | return None 84 | 85 | def pad_length(self, var1, var2, pad = "0"): 86 | # Pads the vars on the left side to make them equal length 87 | pad = "0" if len(str(pad)) < 1 else str(pad)[0] 88 | if not type(var1) == type(var2): 89 | # Type mismatch! Just return what we got 90 | return (var1, var2) 91 | if len(var1) < len(var2): 92 | if type(var1) is list: 93 | var1.extend([str(pad) for x in range(len(var2) - len(var1))]) 94 | else: 95 | var1 = "{}{}".format((pad*(len(var2)-len(var1))), var1) 96 | elif len(var2) < len(var1): 97 | if type(var2) is list: 98 | var2.extend([str(pad) for x in range(len(var1) - len(var2))]) 99 | else: 100 | var2 = "{}{}".format((pad*(len(var1)-len(var2))), var2) 101 | return (var1, var2) 102 | 103 | def check_path(self, path): 104 | # Let's loop until we either get a working path, or no changes 105 | test_path = path 106 | last_path = None 107 | while True: 108 | # Bail if we've looped at least once and the path didn't change 109 | if last_path != None and last_path == test_path: return None 110 | last_path = test_path 111 | # Check if we stripped everything out 112 | if not len(test_path): return None 113 | # Check if we have a valid path 114 | if os.path.exists(test_path): 115 | return os.path.abspath(test_path) 116 | # Check for quotes 117 | if test_path[0] == test_path[-1] and test_path[0] in ('"',"'"): 118 | test_path = test_path[1:-1] 119 | continue 120 | # Check for a tilde and expand if needed 121 | if test_path[0] == "~": 122 | tilde_expanded = os.path.expanduser(test_path) 123 | if tilde_expanded != test_path: 124 | # Got a change 125 | test_path = tilde_expanded 126 | continue 127 | # Let's check for spaces - strip from the left first, then the right 128 | if test_path[0] in (" ","\t"): 129 | test_path = test_path[1:] 130 | continue 131 | if test_path[-1] in (" ","\t"): 132 | test_path = test_path[:-1] 133 | continue 134 | # Maybe we have escapes to handle? 135 | test_path = "\\".join([x.replace("\\", "") for x in test_path.split("\\\\")]) 136 | 137 | def grab(self, prompt, **kwargs): 138 | # Takes a prompt, a default, and a timeout and shows it with that timeout 139 | # returning the result 140 | timeout = kwargs.get("timeout",0) 141 | default = kwargs.get("default","") 142 | if not self.interactive: 143 | return default 144 | # If we don't have a timeout - then skip the timed sections 145 | if timeout <= 0: 146 | try: 147 | if sys.version_info >= (3, 0): 148 | return input(prompt) 149 | else: 150 | return str(raw_input(prompt)) 151 | except EOFError: 152 | return default 153 | # Write our prompt 154 | sys.stdout.write(prompt) 155 | sys.stdout.flush() 156 | if os.name == "nt": 157 | start_time = time.time() 158 | i = '' 159 | while True: 160 | if msvcrt.kbhit(): 161 | c = msvcrt.getche() 162 | if ord(c) == 13: # enter_key 163 | break 164 | elif ord(c) >= 32: # space_char 165 | i += c.decode() if sys.version_info >= (3,0) and isinstance(c,bytes) else c 166 | else: 167 | time.sleep(0.02) # Delay for 20ms to prevent CPU workload 168 | if len(i) == 0 and (time.time() - start_time) > timeout: 169 | break 170 | else: 171 | i, o, e = select.select( [sys.stdin], [], [], timeout ) 172 | if i: 173 | i = sys.stdin.readline().strip() 174 | print('') # needed to move to next line 175 | if len(i) > 0: 176 | return i 177 | else: 178 | return default 179 | 180 | def cls(self): 181 | if not self.interactive: 182 | return 183 | if os.name == "nt": 184 | os.system("cls") 185 | elif os.environ.get("TERM"): 186 | os.system("clear") 187 | 188 | def cprint(self, message, **kwargs): 189 | strip_colors = kwargs.get("strip_colors", False) 190 | if os.name == "nt" or not self.interactive: 191 | strip_colors = True 192 | reset = u"\u001b[0m" 193 | # Requires sys import 194 | for c in self.colors: 195 | if strip_colors: 196 | message = message.replace(c["find"], "") 197 | else: 198 | message = message.replace(c["find"], c["replace"]) 199 | if strip_colors: 200 | return message 201 | sys.stdout.write(message) 202 | print(reset) 203 | 204 | # Needs work to resize the string if color chars exist 205 | '''# Header drawing method 206 | def head(self, text = None, width = 55): 207 | if text == None: 208 | text = self.name 209 | self.cls() 210 | print(" {}".format("#"*width)) 211 | len_text = self.cprint(text, strip_colors=True) 212 | mid_len = int(round(width/2-len(len_text)/2)-2) 213 | middle = " #{}{}{}#".format(" "*mid_len, len_text, " "*((width - mid_len - len(len_text))-2)) 214 | if len(middle) > width+1: 215 | # Get the difference 216 | di = len(middle) - width 217 | # Add the padding for the ...# 218 | di += 3 219 | # Trim the string 220 | middle = middle[:-di] 221 | newlen = len(middle) 222 | middle += "...#" 223 | find_list = [ c["find"] for c in self.colors ] 224 | 225 | # Translate colored string to len 226 | middle = middle.replace(len_text, text + self.rt_color) # always reset just in case 227 | self.cprint(middle) 228 | print("#"*width)''' 229 | 230 | # Header drawing method 231 | def head(self, text = None, width = 55): 232 | if not self.interactive: 233 | sys.stderr.write(str(text)+"\n") 234 | sys.stderr.flush() 235 | return 236 | if text is None: 237 | text = self.name 238 | self.cls() 239 | print(" {}".format("#"*width)) 240 | mid_len = int(round(width/2-len(text)/2)-2) 241 | middle = " #{}{}{}#".format(" "*mid_len, text, " "*((width - mid_len - len(text))-2)) 242 | if len(middle) > width+1: 243 | # Get the difference 244 | di = len(middle) - width 245 | # Add the padding for the ...# 246 | di += 3 247 | # Trim the string 248 | middle = middle[:-di] + "...#" 249 | print(middle) 250 | print("#"*width) 251 | print("") 252 | 253 | def info(self, text): 254 | if self.interactive: 255 | print(text) 256 | else: 257 | sys.stderr.write(str(text)+"\n") 258 | sys.stderr.flush() 259 | 260 | def resize(self, width, height): 261 | print('\033[8;{};{}t'.format(height, width)) 262 | 263 | def custom_quit(self): 264 | self.head() 265 | print("by CorpNewt\n") 266 | print("Thanks for testing it out, for bugs/comments/complaints") 267 | print("send me a message on Reddit, or check out my GitHub:\n") 268 | print("www.reddit.com/u/corpnewt") 269 | print("www.github.com/corpnewt\n") 270 | # Get the time and wish them a good morning, afternoon, evening, and night 271 | hr = datetime.datetime.now().time().hour 272 | if hr > 3 and hr < 12: 273 | print("Have a nice morning!\n\n") 274 | elif hr >= 12 and hr < 17: 275 | print("Have a nice afternoon!\n\n") 276 | elif hr >= 17 and hr < 21: 277 | print("Have a nice evening!\n\n") 278 | else: 279 | print("Have a nice night!\n\n") 280 | exit(0) 281 | -------------------------------------------------------------------------------- /gibMacOS.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM Get our local path before delayed expansion - allows ! in path 3 | set "thisDir=%~dp0" 4 | 5 | setlocal enableDelayedExpansion 6 | REM Setup initial vars 7 | set "script_name=" 8 | set /a tried=0 9 | set "toask=yes" 10 | set "pause_on_error=yes" 11 | set "py2v=" 12 | set "py2path=" 13 | set "py3v=" 14 | set "py3path=" 15 | set "pypath=" 16 | set "targetpy=3" 17 | 18 | REM use_py3: 19 | REM TRUE = Use if found, use py2 otherwise 20 | REM FALSE = Use py2 21 | REM FORCE = Use py3 22 | set "use_py3=TRUE" 23 | 24 | REM We'll parse if the first argument passed is 25 | REM --install-python and if so, we'll just install 26 | set "just_installing=FALSE" 27 | 28 | REM Get the system32 (or equivalent) path 29 | call :getsyspath "syspath" 30 | 31 | REM Make sure the syspath exists 32 | if "!syspath!" == "" ( 33 | if exist "%SYSTEMROOT%\system32\cmd.exe" ( 34 | if exist "%SYSTEMROOT%\system32\reg.exe" ( 35 | if exist "%SYSTEMROOT%\system32\where.exe" ( 36 | REM Fall back on the default path if it exists 37 | set "ComSpec=%SYSTEMROOT%\system32\cmd.exe" 38 | set "syspath=%SYSTEMROOT%\system32\" 39 | ) 40 | ) 41 | ) 42 | if "!syspath!" == "" ( 43 | cls 44 | echo ### ### 45 | echo # Warning # 46 | echo ### ### 47 | echo. 48 | echo Could not locate cmd.exe, reg.exe, or where.exe 49 | echo. 50 | echo Please ensure your ComSpec environment variable is properly configured and 51 | echo points directly to cmd.exe, then try again. 52 | echo. 53 | echo Current CompSpec Value: "%ComSpec%" 54 | echo. 55 | echo Press [enter] to quit. 56 | pause > nul 57 | exit /b 1 58 | ) 59 | ) 60 | 61 | if "%~1" == "--install-python" ( 62 | set "just_installing=TRUE" 63 | goto installpy 64 | ) 65 | 66 | goto checkscript 67 | 68 | :checkscript 69 | REM Check for our script first 70 | set "looking_for=!script_name!" 71 | if "!script_name!" == "" ( 72 | set "looking_for=%~n0.py or %~n0.command" 73 | set "script_name=%~n0.py" 74 | if not exist "!thisDir!\!script_name!" ( 75 | set "script_name=%~n0.command" 76 | ) 77 | ) 78 | if not exist "!thisDir!\!script_name!" ( 79 | echo Could not find !looking_for!. 80 | echo Please make sure to run this script from the same directory 81 | echo as !looking_for!. 82 | echo. 83 | echo Press [enter] to quit. 84 | pause > nul 85 | exit /b 1 86 | ) 87 | goto checkpy 88 | 89 | :checkpy 90 | call :updatepath 91 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe python 2^> nul`) do ( call :checkpyversion "%%x" "py2v" "py2path" "py3v" "py3path" ) 92 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe python3 2^> nul`) do ( call :checkpyversion "%%x" "py2v" "py2path" "py3v" "py3path" ) 93 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe py 2^> nul`) do ( call :checkpylauncher "%%x" "py2v" "py2path" "py3v" "py3path" ) 94 | REM Walk our returns to see if we need to install 95 | if /i "!use_py3!" == "FALSE" ( 96 | set "targetpy=2" 97 | set "pypath=!py2path!" 98 | ) else if /i "!use_py3!" == "FORCE" ( 99 | set "pypath=!py3path!" 100 | ) else if /i "!use_py3!" == "TRUE" ( 101 | set "pypath=!py3path!" 102 | if "!pypath!" == "" set "pypath=!py2path!" 103 | ) 104 | if not "!pypath!" == "" ( 105 | goto runscript 106 | ) 107 | if !tried! lss 1 ( 108 | if /i "!toask!"=="yes" ( 109 | REM Better ask permission first 110 | goto askinstall 111 | ) else ( 112 | goto installpy 113 | ) 114 | ) else ( 115 | cls 116 | echo ### ### 117 | echo # Warning # 118 | echo ### ### 119 | echo. 120 | REM Couldn't install for whatever reason - give the error message 121 | echo Python is not installed or not found in your PATH var. 122 | echo Please install it from https://www.python.org/downloads/windows/ 123 | echo. 124 | echo Make sure you check the box labeled: 125 | echo. 126 | echo "Add Python X.X to PATH" 127 | echo. 128 | echo Where X.X is the py version you're installing. 129 | echo. 130 | echo Press [enter] to quit. 131 | pause > nul 132 | exit /b 1 133 | ) 134 | goto runscript 135 | 136 | :checkpylauncher 137 | REM Attempt to check the latest python 2 and 3 versions via the py launcher 138 | for /f "USEBACKQ tokens=*" %%x in (`%~1 -2 -c "import sys; print(sys.executable)" 2^> nul`) do ( call :checkpyversion "%%x" "%~2" "%~3" "%~4" "%~5" ) 139 | for /f "USEBACKQ tokens=*" %%x in (`%~1 -3 -c "import sys; print(sys.executable)" 2^> nul`) do ( call :checkpyversion "%%x" "%~2" "%~3" "%~4" "%~5" ) 140 | goto :EOF 141 | 142 | :checkpyversion 143 | set "version="&for /f "tokens=2* USEBACKQ delims= " %%a in (`"%~1" -V 2^>^&1`) do ( 144 | REM Ensure we have a version number 145 | call :isnumber "%%a" 146 | if not "!errorlevel!" == "0" goto :EOF 147 | set "version=%%a" 148 | ) 149 | if not defined version goto :EOF 150 | if "!version:~0,1!" == "2" ( 151 | REM Python 2 152 | call :comparepyversion "!version!" "!%~2!" 153 | if "!errorlevel!" == "1" ( 154 | set "%~2=!version!" 155 | set "%~3=%~1" 156 | ) 157 | ) else ( 158 | REM Python 3 159 | call :comparepyversion "!version!" "!%~4!" 160 | if "!errorlevel!" == "1" ( 161 | set "%~4=!version!" 162 | set "%~5=%~1" 163 | ) 164 | ) 165 | goto :EOF 166 | 167 | :isnumber 168 | set "var="&for /f "delims=0123456789." %%i in ("%~1") do set var=%%i 169 | if defined var (exit /b 1) 170 | exit /b 0 171 | 172 | :comparepyversion 173 | REM Exits with status 0 if equal, 1 if v1 gtr v2, 2 if v1 lss v2 174 | for /f "tokens=1,2,3 delims=." %%a in ("%~1") do ( 175 | set a1=%%a 176 | set a2=%%b 177 | set a3=%%c 178 | ) 179 | for /f "tokens=1,2,3 delims=." %%a in ("%~2") do ( 180 | set b1=%%a 181 | set b2=%%b 182 | set b3=%%c 183 | ) 184 | if not defined a1 set a1=0 185 | if not defined a2 set a2=0 186 | if not defined a3 set a3=0 187 | if not defined b1 set b1=0 188 | if not defined b2 set b2=0 189 | if not defined b3 set b3=0 190 | if %a1% gtr %b1% exit /b 1 191 | if %a1% lss %b1% exit /b 2 192 | if %a2% gtr %b2% exit /b 1 193 | if %a2% lss %b2% exit /b 2 194 | if %a3% gtr %b3% exit /b 1 195 | if %a3% lss %b3% exit /b 2 196 | exit /b 0 197 | 198 | :askinstall 199 | cls 200 | echo ### ### 201 | echo # Python Not Found # 202 | echo ### ### 203 | echo. 204 | echo Python !targetpy! was not found on the system or in the PATH var. 205 | echo. 206 | set /p "menu=Would you like to install it now? [y/n]: " 207 | if /i "!menu!"=="y" ( 208 | REM We got the OK - install it 209 | goto installpy 210 | ) else if "!menu!"=="n" ( 211 | REM No OK here... 212 | set /a tried=!tried!+1 213 | goto checkpy 214 | ) 215 | REM Incorrect answer - go back 216 | goto askinstall 217 | 218 | :installpy 219 | REM This will attempt to download and install python 220 | REM First we get the html for the python downloads page for Windows 221 | set /a tried=!tried!+1 222 | cls 223 | echo ### ### 224 | echo # Installing Python # 225 | echo ### ### 226 | echo. 227 | echo Gathering info from https://www.python.org/downloads/windows/... 228 | powershell -command "[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12;(new-object System.Net.WebClient).DownloadFile('https://www.python.org/downloads/windows/','%TEMP%\pyurl.txt')" 229 | REM Extract it if it's gzip compressed 230 | powershell -command "$infile='%TEMP%\pyurl.txt';$outfile='%TEMP%\pyurl.temp';try{$input=New-Object System.IO.FileStream $infile,([IO.FileMode]::Open),([IO.FileAccess]::Read),([IO.FileShare]::Read);$output=New-Object System.IO.FileStream $outfile,([IO.FileMode]::Create),([IO.FileAccess]::Write),([IO.FileShare]::None);$gzipStream=New-Object System.IO.Compression.GzipStream $input,([IO.Compression.CompressionMode]::Decompress);$buffer=New-Object byte[](1024);while($true){$read=$gzipstream.Read($buffer,0,1024);if($read -le 0){break};$output.Write($buffer,0,$read)};$gzipStream.Close();$output.Close();$input.Close();Move-Item -Path $outfile -Destination $infile -Force}catch{}" 231 | if not exist "%TEMP%\pyurl.txt" ( 232 | if /i "!just_installing!" == "TRUE" ( 233 | echo Failed to get info 234 | exit /b 1 235 | ) else ( 236 | goto checkpy 237 | ) 238 | ) 239 | echo Parsing for latest... 240 | pushd "%TEMP%" 241 | :: Version detection code slimmed by LussacZheng (https://github.com/corpnewt/gibMacOS/issues/20) 242 | for /f "tokens=9 delims=< " %%x in ('findstr /i /c:"Latest Python !targetpy! Release" pyurl.txt') do ( set "release=%%x" ) 243 | popd 244 | if "!release!" == "" ( 245 | if /i "!just_installing!" == "TRUE" ( 246 | echo Failed to get python version 247 | exit /b 1 248 | ) else ( 249 | goto checkpy 250 | ) 251 | ) 252 | echo Found Python !release! - Downloading... 253 | REM Let's delete our txt file now - we no longer need it 254 | del "%TEMP%\pyurl.txt" 255 | REM At this point - we should have the version number. 256 | REM We can build the url like so: "https://www.python.org/ftp/python/[version]/python-[version]-amd64.exe" 257 | set "url=https://www.python.org/ftp/python/!release!/python-!release!-amd64.exe" 258 | set "pytype=exe" 259 | if "!targetpy!" == "2" ( 260 | set "url=https://www.python.org/ftp/python/!release!/python-!release!.amd64.msi" 261 | set "pytype=msi" 262 | ) 263 | REM Now we download it with our slick powershell command 264 | powershell -command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (new-object System.Net.WebClient).DownloadFile('!url!','%TEMP%\pyinstall.!pytype!')" 265 | REM If it doesn't exist - we bail 266 | if not exist "%TEMP%\pyinstall.!pytype!" ( 267 | if /i "!just_installing!" == "TRUE" ( 268 | echo Failed to download installer 269 | exit /b 1 270 | ) else ( 271 | goto checkpy 272 | ) 273 | ) 274 | REM It should exist at this point - let's run it to install silently 275 | echo Installing... 276 | pushd "%TEMP%" 277 | if /i "!pytype!" == "exe" ( 278 | echo pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 279 | pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 280 | ) else ( 281 | set "foldername=!release:.=!" 282 | echo msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR="%LocalAppData%\Programs\Python\Python!foldername:~0,2!" 283 | msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR="%LocalAppData%\Programs\Python\Python!foldername:~0,2!" 284 | ) 285 | popd 286 | echo Installer finished with %ERRORLEVEL% status. 287 | REM Now we should be able to delete the installer and check for py again 288 | del "%TEMP%\pyinstall.!pytype!" 289 | REM If it worked, then we should have python in our PATH 290 | REM this does not get updated right away though - let's try 291 | REM manually updating the local PATH var 292 | call :updatepath 293 | if /i "!just_installing!" == "TRUE" ( 294 | echo. 295 | echo Done. 296 | ) else ( 297 | goto checkpy 298 | ) 299 | exit /b 300 | 301 | :runscript 302 | REM Python found 303 | cls 304 | set "args=%*" 305 | set "args=!args:"=!" 306 | if "!args!"=="" ( 307 | "!pypath!" "!thisDir!!script_name!" 308 | ) else ( 309 | "!pypath!" "!thisDir!!script_name!" %* 310 | ) 311 | if /i "!pause_on_error!" == "yes" ( 312 | if not "%ERRORLEVEL%" == "0" ( 313 | echo. 314 | echo Script exited with error code: %ERRORLEVEL% 315 | echo. 316 | echo Press [enter] to exit... 317 | pause > nul 318 | ) 319 | ) 320 | goto :EOF 321 | 322 | :undouble 323 | REM Helper function to strip doubles of a single character out of a string recursively 324 | set "string_value=%~2" 325 | :undouble_continue 326 | set "check=!string_value:%~3%~3=%~3!" 327 | if not "!check!" == "!string_value!" ( 328 | set "string_value=!check!" 329 | goto :undouble_continue 330 | ) 331 | set "%~1=!check!" 332 | goto :EOF 333 | 334 | :updatepath 335 | set "spath=" 336 | set "upath=" 337 | for /f "USEBACKQ tokens=2* delims= " %%i in (`!syspath!reg.exe query "HKCU\Environment" /v "Path" 2^> nul`) do ( if not "%%j" == "" set "upath=%%j" ) 338 | for /f "USEBACKQ tokens=2* delims= " %%i in (`!syspath!reg.exe query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "Path" 2^> nul`) do ( if not "%%j" == "" set "spath=%%j" ) 339 | if not "%spath%" == "" ( 340 | REM We got something in the system path 341 | set "PATH=%spath%" 342 | if not "%upath%" == "" ( 343 | REM We also have something in the user path 344 | set "PATH=%PATH%;%upath%" 345 | ) 346 | ) else if not "%upath%" == "" ( 347 | set "PATH=%upath%" 348 | ) 349 | REM Remove double semicolons from the adjusted PATH 350 | call :undouble "PATH" "%PATH%" ";" 351 | goto :EOF 352 | 353 | :getsyspath 354 | REM Helper method to return a valid path to cmd.exe, reg.exe, and where.exe by 355 | REM walking the ComSpec var - will also repair it in memory if need be 356 | REM Strip double semi-colons 357 | call :undouble "temppath" "%ComSpec%" ";" 358 | 359 | REM Dirty hack to leverage the "line feed" approach - there are some odd side 360 | REM effects with this. Do not use this variable name in comments near this 361 | REM line - as it seems to behave erradically. 362 | (set LF=^ 363 | %=this line is empty=% 364 | ) 365 | REM Replace instances of semi-colons with a line feed and wrap 366 | REM in parenthesis to work around some strange batch behavior 367 | set "testpath=%temppath:;=!LF!%" 368 | 369 | REM Let's walk each path and test if cmd.exe, reg.exe, and where.exe exist there 370 | set /a found=0 371 | for /f "tokens=* delims=" %%i in ("!testpath!") do ( 372 | REM Only continue if we haven't found it yet 373 | if not "%%i" == "" ( 374 | if !found! lss 1 ( 375 | set "checkpath=%%i" 376 | REM Remove "cmd.exe" from the end if it exists 377 | if /i "!checkpath:~-7!" == "cmd.exe" ( 378 | set "checkpath=!checkpath:~0,-7!" 379 | ) 380 | REM Pad the end with a backslash if needed 381 | if not "!checkpath:~-1!" == "\" ( 382 | set "checkpath=!checkpath!\" 383 | ) 384 | REM Let's see if cmd, reg, and where exist there - and set it if so 385 | if EXIST "!checkpath!cmd.exe" ( 386 | if EXIST "!checkpath!reg.exe" ( 387 | if EXIST "!checkpath!where.exe" ( 388 | set /a found=1 389 | set "ComSpec=!checkpath!cmd.exe" 390 | set "%~1=!checkpath!" 391 | ) 392 | ) 393 | ) 394 | ) 395 | ) 396 | ) 397 | goto :EOF 398 | -------------------------------------------------------------------------------- /gibMacOS.command: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the curent directory, the script name 4 | # and the script name with "py" substituted for the extension. 5 | args=( "$@" ) 6 | dir="$(cd -- "$(dirname "$0")" >/dev/null 2>&1; pwd -P)" 7 | script="${0##*/}" 8 | target="${script%.*}.py" 9 | 10 | # use_py3: 11 | # TRUE = Use if found, use py2 otherwise 12 | # FALSE = Use py2 13 | # FORCE = Use py3 14 | use_py3="TRUE" 15 | 16 | # We'll parse if the first argument passed is 17 | # --install-python and if so, we'll just install 18 | just_installing="FALSE" 19 | 20 | tempdir="" 21 | 22 | compare_to_version () { 23 | # Compares our OS version to the passed OS version, and 24 | # return a 1 if we match the passed compare type, or a 0 if we don't. 25 | # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal) 26 | # $2 = OS version to compare ours to 27 | if [ -z "$1" ] || [ -z "$2" ]; then 28 | # Missing info - bail. 29 | return 30 | fi 31 | local current_os= comp= 32 | current_os="$(sw_vers -productVersion)" 33 | comp="$(vercomp "$current_os" "$2")" 34 | # Check gequal and lequal first 35 | if [[ "$1" == "3" && ("$comp" == "1" || "$comp" == "0") ]] || [[ "$1" == "4" && ("$comp" == "2" || "$comp" == "0") ]] || [[ "$comp" == "$1" ]]; then 36 | # Matched 37 | echo "1" 38 | else 39 | # No match 40 | echo "0" 41 | fi 42 | } 43 | 44 | set_use_py3_if () { 45 | # Auto sets the "use_py3" variable based on 46 | # conditions passed 47 | # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal) 48 | # $2 = OS version to compare 49 | # $3 = TRUE/FALSE/FORCE in case of match 50 | if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then 51 | # Missing vars - bail with no changes. 52 | return 53 | fi 54 | if [ "$(compare_to_version "$1" "$2")" == "1" ]; then 55 | use_py3="$3" 56 | fi 57 | } 58 | 59 | get_remote_py_version () { 60 | local pyurl= py_html= py_vers= py_num="3" 61 | pyurl="https://www.python.org/downloads/macos/" 62 | py_html="$(curl -L $pyurl --compressed 2>&1)" 63 | if [ -z "$use_py3" ]; then 64 | use_py3="TRUE" 65 | fi 66 | if [ "$use_py3" == "FALSE" ]; then 67 | py_num="2" 68 | fi 69 | py_vers="$(echo "$py_html" | grep -i "Latest Python $py_num Release" | awk '{print $8}' | cut -d'<' -f1)" 70 | echo "$py_vers" 71 | } 72 | 73 | download_py () { 74 | local vers="$1" url= 75 | clear 76 | echo " ### ###" 77 | echo " # Downloading Python #" 78 | echo "### ###" 79 | echo 80 | if [ -z "$vers" ]; then 81 | echo "Gathering latest version..." 82 | vers="$(get_remote_py_version)" 83 | fi 84 | if [ -z "$vers" ]; then 85 | # Didn't get it still - bail 86 | print_error 87 | fi 88 | echo "Located Version: $vers" 89 | echo 90 | echo "Building download url..." 91 | url="$(curl -L https://www.python.org/downloads/release/python-${vers//./}/ --compressed 2>&1 | grep -iE "python-$vers-macos.*.pkg\"" | awk -F'"' '{ print $2 }')" 92 | if [ -z "$url" ]; then 93 | # Couldn't get the URL - bail 94 | print_error 95 | fi 96 | echo " - $url" 97 | echo 98 | echo "Downloading..." 99 | echo 100 | # Create a temp dir and download to it 101 | tempdir="$(mktemp -d 2>/dev/null || mktemp -d -t 'tempdir')" 102 | curl "$url" -o "$tempdir/python.pkg" 103 | if [ "$?" != "0" ]; then 104 | echo 105 | echo " - Failed to download python installer!" 106 | echo 107 | exit $? 108 | fi 109 | echo 110 | echo "Running python install package..." 111 | echo 112 | sudo installer -pkg "$tempdir/python.pkg" -target / 113 | if [ "$?" != "0" ]; then 114 | echo 115 | echo " - Failed to install python!" 116 | echo 117 | exit $? 118 | fi 119 | # Now we expand the package and look for a shell update script 120 | pkgutil --expand "$tempdir/python.pkg" "$tempdir/python" 121 | if [ -e "$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall" ]; then 122 | # Run the script 123 | echo 124 | echo "Updating PATH..." 125 | echo 126 | "$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall" 127 | fi 128 | vers_folder="Python $(echo "$vers" | cut -d'.' -f1 -f2)" 129 | if [ -f "/Applications/$vers_folder/Install Certificates.command" ]; then 130 | # Certs script exists - let's execute that to make sure our certificates are updated 131 | echo 132 | echo "Updating Certificates..." 133 | echo 134 | "/Applications/$vers_folder/Install Certificates.command" 135 | fi 136 | echo 137 | echo "Cleaning up..." 138 | cleanup 139 | echo 140 | if [ "$just_installing" == "TRUE" ]; then 141 | echo "Done." 142 | else 143 | # Now we check for py again 144 | echo "Rechecking py..." 145 | downloaded="TRUE" 146 | clear 147 | main 148 | fi 149 | } 150 | 151 | cleanup () { 152 | if [ -d "$tempdir" ]; then 153 | rm -Rf "$tempdir" 154 | fi 155 | } 156 | 157 | print_error() { 158 | clear 159 | cleanup 160 | echo " ### ###" 161 | echo " # Python Not Found #" 162 | echo "### ###" 163 | echo 164 | echo "Python is not installed or not found in your PATH var." 165 | echo 166 | if [ "$kernel" == "Darwin" ]; then 167 | echo "Please go to https://www.python.org/downloads/macos/ to" 168 | echo "download and install the latest version, then try again." 169 | else 170 | echo "Please install python through your package manager and" 171 | echo "try again." 172 | fi 173 | echo 174 | exit 1 175 | } 176 | 177 | print_target_missing() { 178 | clear 179 | cleanup 180 | echo " ### ###" 181 | echo " # Target Not Found #" 182 | echo "### ###" 183 | echo 184 | echo "Could not locate $target!" 185 | echo 186 | exit 1 187 | } 188 | 189 | format_version () { 190 | local vers="$1" 191 | echo "$(echo "$1" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }')" 192 | } 193 | 194 | vercomp () { 195 | # Modified from: https://apple.stackexchange.com/a/123408/11374 196 | local ver1="$(format_version "$1")" ver2="$(format_version "$2")" 197 | if [ $ver1 -gt $ver2 ]; then 198 | echo "1" 199 | elif [ $ver1 -lt $ver2 ]; then 200 | echo "2" 201 | else 202 | echo "0" 203 | fi 204 | } 205 | 206 | get_local_python_version() { 207 | # $1 = Python bin name (defaults to python3) 208 | # Echoes the path to the highest version of the passed python bin if any 209 | local py_name="$1" max_version= python= python_version= python_path= 210 | if [ -z "$py_name" ]; then 211 | py_name="python3" 212 | fi 213 | py_list="$(which -a "$py_name" 2>/dev/null)" 214 | # Walk that newline separated list 215 | while read python; do 216 | if [ -z "$python" ]; then 217 | # Got a blank line - skip 218 | continue 219 | fi 220 | if [ "$check_py3_stub" == "1" ] && [ "$python" == "/usr/bin/python3" ]; then 221 | # See if we have a valid developer path 222 | xcode-select -p > /dev/null 2>&1 223 | if [ "$?" != "0" ]; then 224 | # /usr/bin/python3 path - but no valid developer dir 225 | continue 226 | fi 227 | fi 228 | python_version="$(get_python_version $python)" 229 | if [ -z "$python_version" ]; then 230 | # Didn't find a py version - skip 231 | continue 232 | fi 233 | # Got the py version - compare to our max 234 | if [ -z "$max_version" ] || [ "$(vercomp "$python_version" "$max_version")" == "1" ]; then 235 | # Max not set, or less than the current - update it 236 | max_version="$python_version" 237 | python_path="$python" 238 | fi 239 | done <<< "$py_list" 240 | echo "$python_path" 241 | } 242 | 243 | get_python_version() { 244 | local py_path="$1" py_version= 245 | # Get the python version by piping stderr into stdout (for py2), then grepping the output for 246 | # the word "python", getting the second element, and grepping for an alphanumeric version number 247 | py_version="$($py_path -V 2>&1 | grep -i python | cut -d' ' -f2 | grep -E "[A-Za-z\d\.]+")" 248 | if [ ! -z "$py_version" ]; then 249 | echo "$py_version" 250 | fi 251 | } 252 | 253 | prompt_and_download() { 254 | if [ "$downloaded" != "FALSE" ] || [ "$kernel" != "Darwin" ]; then 255 | # We already tried to download, or we're not on macOS - just bail 256 | print_error 257 | fi 258 | clear 259 | echo " ### ###" 260 | echo " # Python Not Found #" 261 | echo "### ###" 262 | echo 263 | target_py="Python 3" 264 | printed_py="Python 2 or 3" 265 | if [ "$use_py3" == "FORCE" ]; then 266 | printed_py="Python 3" 267 | elif [ "$use_py3" == "FALSE" ]; then 268 | target_py="Python 2" 269 | printed_py="Python 2" 270 | fi 271 | echo "Could not locate $printed_py!" 272 | echo 273 | echo "This script requires $printed_py to run." 274 | echo 275 | while true; do 276 | read -p "Would you like to install the latest $target_py now? (y/n): " yn 277 | case $yn in 278 | [Yy]* ) download_py;break;; 279 | [Nn]* ) print_error;; 280 | esac 281 | done 282 | } 283 | 284 | main() { 285 | local python= version= 286 | # Verify our target exists 287 | if [ ! -f "$dir/$target" ]; then 288 | # Doesn't exist 289 | print_target_missing 290 | fi 291 | if [ -z "$use_py3" ]; then 292 | use_py3="TRUE" 293 | fi 294 | if [ "$use_py3" != "FALSE" ]; then 295 | # Check for py3 first 296 | python="$(get_local_python_version python3)" 297 | fi 298 | if [ "$use_py3" != "FORCE" ] && [ -z "$python" ]; then 299 | # We aren't using py3 explicitly, and we don't already have a path 300 | python="$(get_local_python_version python2)" 301 | if [ -z "$python" ]; then 302 | # Try just looking for "python" 303 | python="$(get_local_python_version python)" 304 | fi 305 | fi 306 | if [ -z "$python" ]; then 307 | # Didn't ever find it - prompt 308 | prompt_and_download 309 | return 1 310 | fi 311 | # Found it - start our script and pass all args 312 | "$python" "$dir/$target" "${args[@]}" 313 | } 314 | 315 | # Keep track of whether or not we're on macOS to determine if 316 | # we can download and install python for the user as needed. 317 | kernel="$(uname -s)" 318 | # Check to see if we need to force based on 319 | # macOS version. 10.15 has a dummy python3 version 320 | # that can trip up some py3 detection in other scripts. 321 | # set_use_py3_if "3" "10.15" "FORCE" 322 | downloaded="FALSE" 323 | # Check for the aforementioned /usr/bin/python3 stub if 324 | # our OS version is 10.15 or greater. 325 | check_py3_stub="$(compare_to_version "3" "10.15")" 326 | trap cleanup EXIT 327 | if [ "$1" == "--install-python" ] && [ "$kernel" == "Darwin" ]; then 328 | just_installing="TRUE" 329 | download_py 330 | else 331 | main 332 | fi 333 | -------------------------------------------------------------------------------- /gibMacOS.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from Scripts import downloader,utils,run,plist 3 | import os, shutil, time, sys, argparse, re, json, subprocess 4 | 5 | class ProgramError(Exception): 6 | def __init__(self, message, title = "Error"): 7 | super(Exception, self).__init__(message) 8 | self.title = title 9 | 10 | 11 | class gibMacOS: 12 | def __init__(self, interactive = True, download_dir = None): 13 | self.interactive = interactive 14 | self.download_dir = download_dir 15 | self.d = downloader.Downloader() 16 | self.u = utils.Utils("gibMacOS", interactive=interactive) 17 | self.r = run.Run() 18 | self.min_w = 80 19 | self.min_h = 24 20 | if os.name == "nt": 21 | self.min_w = 120 22 | self.min_h = 30 23 | self.resize() 24 | 25 | self.catalog_suffix = { 26 | "public" : "beta", 27 | "publicrelease" : "", 28 | "customer" : "customerseed", 29 | "developer" : "seed" 30 | } 31 | 32 | # Load settings.json if it exists in the Scripts folder 33 | self.settings_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"Scripts","settings.json") 34 | self.settings = {} 35 | if os.path.exists(self.settings_path): 36 | try: self.settings = json.load(open(self.settings_path)) 37 | except: pass 38 | 39 | # Load prod_cache.json if it exists in the Scripts folder 40 | self.prod_cache_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"Scripts","prod_cache.plist") 41 | self.prod_cache = {} 42 | if os.path.exists(self.prod_cache_path): 43 | try: 44 | with open(self.prod_cache_path,"rb") as f: 45 | self.prod_cache = plist.load(f) 46 | assert isinstance(self.prod_cache,dict) 47 | except: 48 | self.prod_cache = {} 49 | 50 | # If > 16, assume X-5, else 10.X 51 | # e.g. 17 = Monterey, 18 = Ventura, 19 = Sonoma, 20 = Sequoia 52 | self.current_macos = self.settings.get("current_macos",20) 53 | self.min_macos = 5 54 | self.print_urls = self.settings.get("print_urls",False) 55 | self.print_json = False 56 | self.hide_pid = self.settings.get("hide_pid",False) 57 | self.mac_os_names_url = { 58 | "8" : "mountainlion", 59 | "7" : "lion", 60 | "6" : "snowleopard", 61 | "5" : "leopard" 62 | } 63 | self.version_names = { 64 | "tiger" : "10.4", 65 | "leopard" : "10.5", 66 | "snow leopard" : "10.6", 67 | "lion" : "10.7", 68 | "mountain lion" : "10.8", 69 | "mavericks" : "10.9", 70 | "yosemite" : "10.10", 71 | "el capitan" : "10.11", 72 | "sierra" : "10.12", 73 | "high sierra" : "10.13", 74 | "mojave" : "10.14", 75 | "catalina" : "10.15", 76 | "big sur" : "11", 77 | "monterey" : "12", 78 | "ventura" : "13", 79 | "sonoma" : "14", 80 | "sequoia" : "15" 81 | } 82 | self.current_catalog = self.settings.get("current_catalog","publicrelease") 83 | self.catalog_data = None 84 | self.scripts = "Scripts" 85 | self.local_catalog = os.path.join(os.path.dirname(os.path.realpath(__file__)),self.scripts,"sucatalog.plist") 86 | self.caffeinate_downloads = self.settings.get("caffeinate_downloads",True) 87 | self.caffeinate_process = None 88 | self.save_local = False 89 | self.force_local = False 90 | self.find_recovery = self.settings.get("find_recovery",False) 91 | self.recovery_suffixes = ( 92 | "RecoveryHDUpdate.pkg", 93 | "RecoveryHDMetaDmg.pkg" 94 | ) 95 | self.settings_to_save = ( 96 | "current_macos", 97 | "current_catalog", 98 | "print_urls", 99 | "find_recovery", 100 | "hide_pid", 101 | "caffeinate_downloads" 102 | ) 103 | self.mac_prods = [] 104 | 105 | def resize(self, width=0, height=0): 106 | if not self.interactive: 107 | return 108 | width = width if width > self.min_w else self.min_w 109 | height = height if height > self.min_h else self.min_h 110 | self.u.resize(width, height) 111 | 112 | def save_settings(self): 113 | # Ensure we're using the latest values 114 | for setting in self.settings_to_save: 115 | self.settings[setting] = getattr(self,setting,None) 116 | try: 117 | json.dump(self.settings,open(self.settings_path,"w"),indent=2) 118 | except Exception as e: 119 | raise ProgramError( 120 | "Failed to save settings to:\n\n{}\n\nWith error:\n\n - {}\n".format(self.settings_path,repr(e)), 121 | title="Error Saving Settings") 122 | 123 | def save_prod_cache(self): 124 | try: 125 | with open(self.prod_cache_path,"wb") as f: 126 | plist.dump(self.prod_cache,f) 127 | except Exception as e: 128 | raise ProgramError( 129 | "Failed to save product cache to:\n\n{}\n\nWith error:\n\n - {}\n".format(self.prod_cache_path,repr(e)), 130 | title="Error Saving Product Cache") 131 | 132 | def set_prods(self): 133 | self.resize() 134 | if not self.get_catalog_data(self.save_local): 135 | message = "The currently selected catalog ({}) was not reachable\n".format(self.current_catalog) 136 | if self.save_local: 137 | message += "and I was unable to locate a valid catalog file at:\n - {}\n".format( 138 | self.local_catalog 139 | ) 140 | message += "Please ensure you have a working internet connection." 141 | raise ProgramError(message, title="Catalog Data Error") 142 | self.u.head("Parsing Data") 143 | self.u.info("Scanning products after catalog download...\n") 144 | self.mac_prods = self.get_dict_for_prods(self.get_installers()) 145 | 146 | def set_catalog(self, catalog): 147 | self.current_catalog = catalog.lower() if catalog.lower() in self.catalog_suffix else "publicrelease" 148 | 149 | def num_to_macos(self,macos_num,for_url=True): 150 | if for_url: # Resolve 8-5 to their names and show Big Sur as 10.16 151 | return self.mac_os_names_url.get(str(macos_num),"10.{}".format(macos_num)) if macos_num <= 16 else str(macos_num-5) 152 | # Return 10.xx for anything Catalina and lower, otherwise 11+ 153 | return "10.{}".format(macos_num) if macos_num <= 15 else str(macos_num-5) 154 | 155 | def macos_to_num(self,macos): 156 | try: 157 | macos_parts = [int(x) for x in macos.split(".")][:2 if macos.startswith("10.") else 1] 158 | if macos_parts[0] == 11: macos_parts = [10,16] # Big sur 159 | except: 160 | return None 161 | if len(macos_parts) > 1: return macos_parts[1] 162 | return 5+macos_parts[0] 163 | 164 | def get_macos_versions(self,minos=None,maxos=None,catalog=""): 165 | if minos is None: minos = self.min_macos 166 | if maxos is None: maxos = self.current_macos 167 | if minos > maxos: minos,maxos = maxos,minos # Ensure min is less than or equal 168 | os_versions = [self.num_to_macos(x,for_url=True) for x in range(minos,maxos+1)] 169 | if catalog: 170 | # We have a custom catalog - prepend the first entry + catalog to the list 171 | custom_cat_entry = os_versions[-1]+catalog 172 | os_versions.append(custom_cat_entry) 173 | return os_versions 174 | 175 | def build_url(self, **kwargs): 176 | catalog = kwargs.get("catalog", self.current_catalog).lower() 177 | catalog = catalog if catalog.lower() in self.catalog_suffix else "publicrelease" 178 | version = int(kwargs.get("version", self.current_macos)) 179 | return "https://swscan.apple.com/content/catalogs/others/index-{}.merged-1.sucatalog".format( 180 | "-".join(reversed(self.get_macos_versions(self.min_macos,version,catalog=self.catalog_suffix.get(catalog,"")))) 181 | ) 182 | 183 | def get_catalog_data(self, local = False): 184 | # Gets the data based on our current_catalog 185 | url = self.build_url(catalog=self.current_catalog, version=self.current_macos) 186 | self.u.head("Downloading Catalog") 187 | if local: 188 | self.u.info("Checking for:\n - {}".format( 189 | self.local_catalog 190 | )) 191 | if os.path.exists(self.local_catalog): 192 | self.u.info(" - Found - loading...") 193 | try: 194 | with open(self.local_catalog, "rb") as f: 195 | self.catalog_data = plist.load(f) 196 | assert isinstance(self.catalog_data,dict) 197 | return True 198 | except Exception as e: 199 | self.u.info(" - Error loading: {}".format(e)) 200 | self.u.info(" - Downloading instead...\n") 201 | else: 202 | self.u.info(" - Not found - downloading instead...\n") 203 | self.u.info("Currently downloading {} catalog from:\n\n{}\n".format(self.current_catalog, url)) 204 | try: 205 | b = self.d.get_bytes(url, self.interactive) 206 | self.u.info("") 207 | self.catalog_data = plist.loads(b) 208 | except: 209 | self.u.info("Error downloading!") 210 | return False 211 | try: 212 | # Assume it's valid data - dump it to a local file 213 | if local or self.force_local: 214 | self.u.info(" - Saving to:\n - {}".format( 215 | self.local_catalog 216 | )) 217 | with open(self.local_catalog, "wb") as f: 218 | plist.dump(self.catalog_data, f) 219 | except Exception as e: 220 | self.u.info(" - Error saving: {}".format(e)) 221 | return False 222 | return True 223 | 224 | def get_installers(self, plist_dict = None): 225 | if not plist_dict: 226 | plist_dict = self.catalog_data 227 | if not plist_dict: 228 | return [] 229 | mac_prods = [] 230 | for p in plist_dict.get("Products", {}): 231 | if not self.find_recovery: 232 | val = plist_dict.get("Products",{}).get(p,{}).get("ExtendedMetaInfo",{}).get("InstallAssistantPackageIdentifiers",{}) 233 | if val.get("OSInstall",{}) == "com.apple.mpkg.OSInstall" or val.get("SharedSupport","").startswith("com.apple.pkg.InstallAssistant"): 234 | mac_prods.append(p) 235 | else: 236 | # Find out if we have any of the recovery_suffixes 237 | if any(x for x in plist_dict.get("Products",{}).get(p,{}).get("Packages",[]) if x["URL"].endswith(self.recovery_suffixes)): 238 | mac_prods.append(p) 239 | return mac_prods 240 | 241 | def get_build_version(self, dist_dict): 242 | build = version = name = "Unknown" 243 | try: 244 | dist_url = dist_dict.get("English",dist_dict.get("en","")) 245 | dist_file = self.d.get_string(dist_url,False) 246 | assert isinstance(dist_file,str) 247 | except: 248 | dist_file = "" 249 | build_search = "macOSProductBuildVersion" if "macOSProductBuildVersion" in dist_file else "BUILD" 250 | vers_search = "macOSProductVersion" if "macOSProductVersion" in dist_file else "VERSION" 251 | try: 252 | build = dist_file.split("{}".format(build_search))[1].split("")[1].split("")[0] 253 | except: 254 | pass 255 | try: 256 | version = dist_file.split("{}".format(vers_search))[1].split("")[1].split("")[0] 257 | except: 258 | pass 259 | try: 260 | name = re.search(r"(.+?)",dist_file).group(1) 261 | except: 262 | pass 263 | try: 264 | # XXX: This is parsing a JavaScript array from the script part of the dist file. 265 | device_ids = re.search(r"var supportedDeviceIDs\s*=\s*\[([^]]+)\];", dist_file)[1] 266 | device_ids = list(set(i.lower() for i in re.findall(r"'([^',]+)'", device_ids))) 267 | except: 268 | device_ids = [] 269 | return (build,version,name,device_ids) 270 | 271 | def get_dict_for_prods(self, prods, plist_dict = None): 272 | plist_dict = plist_dict or self.catalog_data or {} 273 | prod_list = [] 274 | # Keys required to consider a cached element valid 275 | prod_keys = ( 276 | "build", 277 | "date", 278 | "description", 279 | "device_ids", 280 | "installer", 281 | "product", 282 | "time", 283 | "title", 284 | "version", 285 | ) 286 | 287 | def get_packages_and_size(plist_dict,prod,recovery): 288 | # Iterate the available packages and save their urls and sizes 289 | packages = [] 290 | size = -1 291 | if recovery: 292 | # Only get the recovery packages 293 | packages = [x for x in plist_dict.get("Products",{}).get(prod,{}).get("Packages",[]) if x["URL"].endswith(self.recovery_suffixes)] 294 | else: 295 | # Add them all! 296 | packages = plist_dict.get("Products",{}).get(prod,{}).get("Packages",[]) 297 | # Get size 298 | size = self.d.get_size(sum([i["Size"] for i in packages])) 299 | return (packages,size) 300 | 301 | def print_prod(prod,prod_list): 302 | self.u.info(" -->{}. {} ({}){}".format( 303 | str(len(prod_list)+1).rjust(3), 304 | prod["title"], 305 | prod["build"], 306 | " - FULL Install" if self.find_recovery and prod["installer"] else "" 307 | )) 308 | 309 | def prod_valid(prod,prod_list,prod_keys): 310 | # Check if the prod has all prod keys, and 311 | # none are "Unknown" 312 | if not isinstance(prod_list,dict) or not prod in prod_list or \ 313 | not all(x in prod_list[prod] for x in prod_keys): 314 | # Wrong type, missing the prod, or prod_list keys 315 | return False 316 | # Let's make sure none of the keys return Unknown 317 | if any(prod_list[prod].get(x,"Unknown")=="Unknown" for x in prod_keys): 318 | return False 319 | return True 320 | 321 | # Boolean to keep track of cache updates 322 | prod_changed = False 323 | for prod in prods: 324 | if prod_valid(prod,self.prod_cache,prod_keys): 325 | # Already have it - and it's valid. 326 | # Create a shallow copy 327 | prodd = {} 328 | for key in self.prod_cache[prod]: 329 | prodd[key] = self.prod_cache[prod][key] 330 | # Update the packages and size lists 331 | prodd["packages"],prodd["size"] = get_packages_and_size(plist_dict,prod,self.find_recovery) 332 | # Add to our list and continue on 333 | prod_list.append(prodd) 334 | # Log the product 335 | print_prod(prodd,prod_list) 336 | continue 337 | # Grab the ServerMetadataURL for the passed product key if it exists 338 | prodd = {"product":prod} 339 | try: 340 | url = plist_dict.get("Products",{}).get(prod,{}).get("ServerMetadataURL","") 341 | assert url 342 | b = self.d.get_bytes(url,False) 343 | smd = plist.loads(b) 344 | except: 345 | smd = {} 346 | # Populate some info! 347 | prodd["date"] = plist_dict.get("Products",{}).get(prod,{}).get("PostDate","") 348 | prodd["installer"] = plist_dict.get("Products",{}).get(prod,{}).get("ExtendedMetaInfo",{}).get("InstallAssistantPackageIdentifiers",{}).get("OSInstall",{}) == "com.apple.mpkg.OSInstall" 349 | prodd["time"] = time.mktime(prodd["date"].timetuple()) + prodd["date"].microsecond / 1E6 350 | prodd["version"] = smd.get("CFBundleShortVersionString","Unknown").strip() 351 | # Try to get the description too 352 | try: 353 | desc = smd.get("localization",{}).get("English",{}).get("description","").decode("utf-8") 354 | desctext = desc.split('"p1">')[1].split("")[0] 355 | except: 356 | desctext = "" 357 | prodd["description"] = desctext 358 | prodd["packages"],prodd["size"] = get_packages_and_size(plist_dict,prod,self.find_recovery) 359 | # Get size 360 | prodd["size"] = self.d.get_size(sum([i["Size"] for i in prodd["packages"]])) 361 | # Attempt to get the build/version/name/device-ids info from the dist 362 | prodd["build"],v,n,prodd["device_ids"] = self.get_build_version(plist_dict.get("Products",{}).get(prod,{}).get("Distributions",{})) 363 | prodd["title"] = smd.get("localization",{}).get("English",{}).get("title",n) 364 | if v.lower() != "unknown": 365 | prodd["version"] = v 366 | prod_list.append(prodd) 367 | # If we were able to resolve the SMD URL - or it didn't exist, save it to the cache 368 | if smd or not plist_dict.get("Products",{}).get(prod,{}).get("ServerMetadataURL",""): 369 | prod_changed = True 370 | # Create a temp prod dict so we can save all but the packages and 371 | # size keys - as those are determined based on self.find_recovery 372 | temp_prod = {} 373 | for key in prodd: 374 | if key in ("packages","size"): continue 375 | if prodd[key] == "Unknown": 376 | # Don't cache Unknown values 377 | temp_prod = None 378 | break 379 | temp_prod[key] = prodd[key] 380 | if temp_prod: 381 | # Only update the cache if it changed 382 | self.prod_cache[prod] = temp_prod 383 | # Log the product 384 | print_prod(prodd,prod_list) 385 | # Try saving the cache for later 386 | if prod_changed and self.prod_cache: 387 | try: self.save_prod_cache() 388 | except: pass 389 | # Sort by newest 390 | prod_list = sorted(prod_list, key=lambda x:x["time"], reverse=True) 391 | return prod_list 392 | 393 | def start_caffeinate(self): 394 | # Check if we need to caffeinate 395 | if sys.platform.lower() == "darwin" \ 396 | and self.caffeinate_downloads \ 397 | and os.path.isfile("/usr/bin/caffeinate"): 398 | # Terminate any existing caffeinate process 399 | self.term_caffeinate_proc() 400 | # Create a new caffeinate process 401 | self.caffeinate_process = subprocess.Popen( 402 | ["/usr/bin/caffeinate"], 403 | stderr=getattr(subprocess,"DEVNULL",open(os.devnull,"w")), 404 | stdout=getattr(subprocess,"DEVNULL",open(os.devnull,"w")), 405 | stdin=getattr(subprocess,"DEVNULL",open(os.devnull,"w")) 406 | ) 407 | return self.caffeinate_process 408 | 409 | def term_caffeinate_proc(self): 410 | if self.caffeinate_process is None: 411 | return True 412 | try: 413 | if self.caffeinate_process.poll() is None: 414 | # Save the time we started waiting 415 | start = time.time() 416 | while self.caffeinate_process.poll() is None: 417 | # Make sure we haven't waited too long 418 | if time.time() - start > 10: 419 | print(" - Timed out trying to terminate caffeinate process with PID {}!".format( 420 | self.caffeinate_process.pid 421 | )) 422 | return False 423 | # It's alive - terminate it 424 | self.caffeinate_process.terminate() 425 | # Sleep to let things settle 426 | time.sleep(0.02) 427 | except: 428 | pass 429 | return True # Couldn't poll - or we termed it 430 | 431 | def download_prod(self, prod, dmg = False): 432 | # Takes a dictonary of details and downloads it 433 | self.resize() 434 | name = "{} - {} {} ({})".format(prod["product"], prod["version"], prod["title"], prod["build"]).replace(":","").strip() 435 | download_dir = self.download_dir or os.path.join(os.path.dirname(os.path.realpath(__file__)), "macOS Downloads", self.current_catalog, name) 436 | dl_list = [] 437 | for x in prod["packages"]: 438 | if not x.get("URL",None): 439 | continue 440 | if dmg and not x.get("URL","").lower().endswith(".dmg"): 441 | continue 442 | # add it to the list 443 | dl_list.append(x) 444 | if not len(dl_list): 445 | raise ProgramError("There were no files to download") 446 | done = [] 447 | if self.print_json: 448 | print(self.product_to_json(prod)) 449 | if self.interactive: 450 | print("") 451 | self.u.grab("Press [enter] to return...") 452 | return 453 | elif self.print_urls: 454 | self.u.head("Download Links") 455 | print("{}:\n".format(name)) 456 | print("\n".join([" - {} ({}) \n --> {}".format( 457 | os.path.basename(x["URL"]), 458 | self.d.get_size(x["Size"],strip_zeroes=True) if x.get("Size") is not None else "?? MB", 459 | x["URL"] 460 | ) for x in dl_list])) 461 | if self.interactive: 462 | print("") 463 | self.u.grab("Press [enter] to return...") 464 | return 465 | # Only check the dirs if we need to 466 | if self.download_dir is None and os.path.exists(download_dir): 467 | while True: 468 | self.u.head("Already Exists") 469 | self.u.info("It looks like you've already downloaded the following package:\n{}\n".format(name)) 470 | if not self.interactive: 471 | menu = "r" 472 | else: 473 | print("R. Resume Incomplete Files") 474 | print("D. Redownload All Files") 475 | print("") 476 | print("M. Return") 477 | print("Q. Quit") 478 | print("") 479 | menu = self.u.grab("Please select an option: ") 480 | if not len(menu): 481 | continue 482 | elif menu.lower() == "q": 483 | self.u.custom_quit() 484 | elif menu.lower() == "m": 485 | return 486 | elif menu.lower() == "r": 487 | break 488 | elif menu.lower() == "d": 489 | # Remove the old copy, then re-download 490 | shutil.rmtree(download_dir) 491 | break 492 | # Make it anew as needed 493 | if not os.path.isdir(download_dir): 494 | os.makedirs(download_dir) 495 | # Clean up any leftover or missed caffeinate 496 | # procs 497 | self.term_caffeinate_proc() 498 | for c,x in enumerate(dl_list,start=1): 499 | url = x["URL"] 500 | self.u.head("Downloading File {} of {}".format(c, len(dl_list))) 501 | self.u.info("- {} -\n".format(name)) 502 | if len(done): 503 | self.u.info("\n".join(["{} --> {}".format(y["name"], "Succeeded" if y["status"] else "Failed") for y in done])) 504 | self.u.info("") 505 | if dmg: 506 | self.u.info("NOTE: Only Downloading DMG Files\n") 507 | self.u.info("Downloading {}...\n".format(os.path.basename(url))) 508 | try: 509 | # Caffeinate as needed 510 | self.start_caffeinate() 511 | result = self.d.stream_to_file(url, os.path.join(download_dir, os.path.basename(url)), allow_resume=True) 512 | assert result is not None 513 | done.append({"name":os.path.basename(url), "status":True}) 514 | except: 515 | done.append({"name":os.path.basename(url), "status":False}) 516 | # Kill caffeinate if we need to 517 | self.term_caffeinate_proc() 518 | succeeded = [x for x in done if x["status"]] 519 | failed = [x for x in done if not x["status"]] 520 | self.u.head("Downloaded {} of {}".format(len(succeeded), len(dl_list))) 521 | self.u.info("- {} -\n".format(name)) 522 | self.u.info("Succeeded:") 523 | if len(succeeded): 524 | for x in succeeded: 525 | self.u.info(" {}".format(x["name"])) 526 | else: 527 | self.u.info(" None") 528 | self.u.info("\nFailed:") 529 | if len(failed): 530 | for x in failed: 531 | self.u.info(" {}".format(x["name"])) 532 | else: 533 | self.u.info(" None") 534 | self.u.info("\nFiles saved to:\n {}\n".format(download_dir)) 535 | if self.interactive: 536 | self.u.grab("Press [enter] to return...") 537 | elif len(failed): 538 | raise ProgramError("{} files failed to download".format(len(failed))) 539 | 540 | def product_to_json(self, prod): 541 | prod_dict = {} 542 | for key in ["product", "version", "build", "title", "size", "packages"]: 543 | if key in prod: 544 | prod_dict[key] = prod[key] 545 | prod_dict["date"] = prod["date"].isoformat() 546 | prod_dict["deviceIds"] = list(prod["device_ids"]) 547 | return json.dumps(prod_dict,indent=2) 548 | 549 | def show_catalog_url(self): 550 | self.resize() 551 | self.u.head() 552 | print("Current Catalog: {}".format(self.current_catalog)) 553 | print("Max macOS Version: {}".format(self.num_to_macos(self.current_macos,for_url=False))) 554 | print("") 555 | print("{}".format(self.build_url())) 556 | if self.interactive: 557 | print("") 558 | self.u.grab("Press [enter] to return...") 559 | 560 | def pick_catalog(self): 561 | self.resize() 562 | self.u.head("Select SU Catalog") 563 | count = 0 564 | for x in self.catalog_suffix: 565 | count += 1 566 | print("{}. {}".format(count, x)) 567 | print("") 568 | print("M. Main Menu") 569 | print("Q. Quit") 570 | print("") 571 | menu = self.u.grab("Please select an option: ") 572 | if not len(menu): 573 | self.pick_catalog() 574 | return 575 | if menu[0].lower() == "m": 576 | return 577 | elif menu[0].lower() == "q": 578 | self.u.custom_quit() 579 | # Should have something to test here 580 | try: 581 | i = int(menu) 582 | self.current_catalog = list(self.catalog_suffix)[i-1] 583 | self.save_settings() 584 | except: 585 | # Incorrect - try again 586 | self.pick_catalog() 587 | return 588 | # If we made it here - then we got something 589 | # Reload with the proper catalog 590 | self.get_catalog_data() 591 | 592 | def pick_macos(self): 593 | self.resize() 594 | self.u.head("Select Max macOS Version") 595 | print("Currently set to {}".format(self.num_to_macos(self.current_macos,for_url=False))) 596 | print("") 597 | print("M. Main Menu") 598 | print("Q. Quit") 599 | print("") 600 | print("Please type the max macOS version for the catalog url") 601 | menu = self.u.grab("eg. 10.15 for Catalina, 11 for Big Sur, 12 for Monterey: ") 602 | if not len(menu): 603 | self.pick_macos() 604 | return 605 | if menu[0].lower() == "m": 606 | return 607 | elif menu[0].lower() == "q": 608 | self.u.custom_quit() 609 | # At this point - we should have something in the proper format 610 | version = self.macos_to_num(menu) 611 | if not version: return 612 | self.current_macos = version 613 | self.save_settings() 614 | # At this point, we should be good 615 | self.get_catalog_data() 616 | 617 | def main(self, dmg = False): 618 | lines = [] 619 | lines.append("Available Products:") 620 | lines.append(" ") 621 | if not len(self.mac_prods): 622 | lines.append("No installers in catalog!") 623 | lines.append(" ") 624 | for num,p in enumerate(self.mac_prods,start=1): 625 | var1 = "{}. {} {}".format(str(num).rjust(2), p["title"], p["version"]) 626 | var2 = "" 627 | if p["build"].lower() != "unknown": 628 | var1 += " ({})".format(p["build"]) 629 | if not self.hide_pid: 630 | var2 = " - {} - Added {} - {}".format(p["product"], p["date"], p["size"]) 631 | if self.find_recovery and p["installer"]: 632 | # Show that it's a full installer 633 | if self.hide_pid: 634 | var1 += " - FULL Install" 635 | else: 636 | var2 += " - FULL Install" 637 | lines.append(var1) 638 | if not self.hide_pid: 639 | lines.append(var2) 640 | lines.append(" ") 641 | lines.append("M. Change Max-OS Version (Currently {})".format(self.num_to_macos(self.current_macos,for_url=False))) 642 | lines.append("C. Change Catalog (Currently {})".format(self.current_catalog)) 643 | lines.append("I. Only Print URLs (Currently {})".format("On" if self.print_urls else "Off")) 644 | lines.append("H. {} Package IDs and Upload Dates".format("Show" if self.hide_pid else "Hide")) 645 | if sys.platform.lower() == "darwin": 646 | lines.append("S. Set Current Catalog to SoftwareUpdate Catalog") 647 | lines.append("L. Clear SoftwareUpdate Catalog") 648 | lines.append("F. Caffeinate Downloads to Prevent Sleep (Currently {})".format("On" if self.caffeinate_downloads else "Off")) 649 | lines.append("R. Toggle Recovery-Only (Currently {})".format("On" if self.find_recovery else "Off")) 650 | lines.append("U. Show Catalog URL") 651 | lines.append("Q. Quit") 652 | lines.append(" ") 653 | self.resize(len(max(lines)), len(lines)+5) 654 | self.u.head() 655 | print("\n".join(lines)) 656 | menu = self.u.grab("Please select an option: ") 657 | if not len(menu): 658 | return 659 | if menu[0].lower() == "q": 660 | self.resize() 661 | self.u.custom_quit() 662 | elif menu[0].lower() == "u": 663 | self.show_catalog_url() 664 | return 665 | elif menu[0].lower() == "m": 666 | self.pick_macos() 667 | elif menu[0].lower() == "c": 668 | self.pick_catalog() 669 | elif menu[0].lower() == "i": 670 | self.print_urls ^= True 671 | self.save_settings() 672 | return 673 | elif menu[0].lower() == "h": 674 | self.hide_pid ^= True 675 | self.save_settings() 676 | elif menu[0].lower() == "s" and sys.platform.lower() == "darwin": 677 | # Set the software update catalog to our current catalog url 678 | self.u.head("Setting SU CatalogURL") 679 | url = self.build_url(catalog=self.current_catalog, version=self.current_macos) 680 | print("Setting catalog URL to:\n{}".format(url)) 681 | print("") 682 | print("sudo softwareupdate --set-catalog {}".format(url)) 683 | self.r.run({"args":["softwareupdate","--set-catalog",url],"sudo":True}) 684 | print("") 685 | self.u.grab("Done",timeout=5) 686 | return 687 | elif menu[0].lower() == "l" and sys.platform.lower() == "darwin": 688 | # Clear the software update catalog 689 | self.u.head("Clearing SU CatalogURL") 690 | print("sudo softwareupdate --clear-catalog") 691 | self.r.run({"args":["softwareupdate","--clear-catalog"],"sudo":True}) 692 | print("") 693 | self.u.grab("Done.", timeout=5) 694 | return 695 | elif menu[0].lower() == "f" and sys.platform.lower() == "darwin": 696 | # Toggle our caffeinate downloads value and save settings 697 | self.caffeinate_downloads ^= True 698 | self.save_settings() 699 | elif menu[0].lower() == "r": 700 | self.find_recovery ^= True 701 | self.save_settings() 702 | if menu[0].lower() in ["m","c","r"]: 703 | self.resize() 704 | self.u.head("Parsing Data") 705 | print("Re-scanning products after url preference toggled...\n") 706 | self.mac_prods = self.get_dict_for_prods(self.get_installers()) 707 | return 708 | 709 | # Assume we picked something 710 | try: 711 | menu = int(menu) 712 | except: 713 | return 714 | if menu < 1 or menu > len(self.mac_prods): 715 | return 716 | self.download_prod(self.mac_prods[menu-1], dmg) 717 | 718 | def get_latest(self, device_id = None, dmg = False): 719 | self.u.head("Downloading Latest") 720 | prods = sorted(self.mac_prods, key=lambda x:x['version'], reverse=True) 721 | if device_id: 722 | prod = next(p for p in prods if device_id.lower() in p["device_ids"]) 723 | if not prod: 724 | raise ProgramError("No version found for Device ID '{}'".format(device_id)) 725 | else: 726 | prod = prods[0] 727 | self.download_prod(prod, dmg) 728 | 729 | def get_for_product(self, prod, dmg = False): 730 | self.u.head("Downloading for {}".format(prod)) 731 | for p in self.mac_prods: 732 | if p["product"] == prod: 733 | self.download_prod(p, dmg) 734 | return 735 | raise ProgramError("{} not found".format(prod)) 736 | 737 | def get_for_version(self, vers, build = None, device_id = None, dmg = False): 738 | self.u.head("Downloading for {} {}".format(vers, build or "")) 739 | # Map the versions to their names 740 | v = self.version_names.get(vers.lower(),vers.lower()) 741 | v_dict = {} 742 | for n in self.version_names: 743 | v_dict[self.version_names[n]] = n 744 | n = v_dict.get(v, v) 745 | for p in sorted(self.mac_prods, key=lambda x:x['version'], reverse=True): 746 | if build and p["build"] != build: 747 | continue 748 | if device_id and device_id.lower() not in p["device_ids"]: 749 | continue 750 | pt = p["title"].lower() 751 | pv = p["version"].lower() 752 | # Need to compare verisons - n = name, v = version 753 | # p["version"] and p["title"] may contain either the version 754 | # or name - so check both 755 | # We want to make sure, if we match the name to the title, that we only match 756 | # once - so Sierra/High Sierra don't cross-match 757 | # 758 | # First check if p["version"] isn't " " or "1.0" 759 | if not pv in [" ","1.0"]: 760 | # Have a real version - match this first 761 | if pv.startswith(v): 762 | self.download_prod(p, dmg) 763 | return 764 | # Didn't match the version - or version was bad, let's check 765 | # the title 766 | # Need to make sure n is in the version name, but not equal to it, 767 | # and the version name is in p["title"] to disqualify 768 | # i.e. - "Sierra" exists in "High Sierra", but does not equal "High Sierra" 769 | # and "High Sierra" is in "macOS High Sierra 10.13.6" - This would match 770 | name_match = [x for x in self.version_names if n in x and x != n and x in pt] 771 | if (n in pt) and not len(name_match): 772 | self.download_prod(p, dmg) 773 | return 774 | raise ProgramError("'{}' '{}' not found".format(vers, build or "")) 775 | 776 | if __name__ == '__main__': 777 | parser = argparse.ArgumentParser() 778 | parser.add_argument("-l", "--latest", help="downloads the version available in the current catalog (overrides --build, --version and --product)", action="store_true") 779 | parser.add_argument("-r", "--recovery", help="looks for RecoveryHDUpdate.pkg and RecoveryHDMetaDmg.pkg in lieu of com.apple.mpkg.OSInstall (overrides --dmg)", action="store_true") 780 | parser.add_argument("-d", "--dmg", help="downloads only the .dmg files", action="store_true") 781 | parser.add_argument("-s", "--savelocal", help="uses a locally saved sucatalog.plist if exists", action="store_true") 782 | parser.add_argument("-g", "--local-catalog", help="the path to the sucatalog.plist to use (implies --savelocal)") 783 | parser.add_argument("-n", "--newlocal", help="downloads and saves locally, overwriting any prior sucatalog.plist (will use the path from --local-catalog if provided)", action="store_true") 784 | parser.add_argument("-c", "--catalog", help="sets the CATALOG to use - publicrelease, public, customer, developer") 785 | parser.add_argument("-p", "--product", help="sets the product id to search for (overrides --version)") 786 | parser.add_argument("-v", "--version", help="sets the version of macOS to target - eg '-v 10.14' or '-v Yosemite'") 787 | parser.add_argument("-b", "--build", help="sets the build of macOS to target - eg '22G120' (must be used together with --version)") 788 | parser.add_argument("-m", "--maxos", help="sets the max macOS version to consider when building the url - eg 10.14") 789 | parser.add_argument("-D", "--device-id", help="use with --version or --latest to search for versions supporting the specified Device ID - eg VMM-x86_64 for any x86_64") 790 | parser.add_argument("-i", "--print-urls", help="only prints the download URLs, does not actually download them", action="store_true") 791 | parser.add_argument("-j", "--print-json", help="only prints the product metadata in JSON, does not actually download it", action="store_true") 792 | parser.add_argument("--no-interactive", help="run in non-interactive mode (auto-enabled when using --product or --version)", action="store_true") 793 | parser.add_argument("-o", "--download-dir", help="overrides directory where the downloaded files are saved") 794 | args = parser.parse_args() 795 | 796 | if args.build and not (args.latest or args.product or args.version): 797 | print("The --build option requires a --version") 798 | exit(1) 799 | 800 | interactive = not any((args.no_interactive,args.product,args.version)) 801 | g = gibMacOS(interactive=interactive, download_dir=args.download_dir) 802 | 803 | if args.recovery: 804 | args.dmg = False 805 | g.find_recovery = args.recovery 806 | 807 | if args.savelocal: 808 | g.save_local = True 809 | 810 | if args.local_catalog: 811 | g.save_local = True 812 | g.local_catalog = args.local_catalog 813 | 814 | if args.newlocal: 815 | g.force_local = True 816 | 817 | if args.print_urls: 818 | g.print_urls = True 819 | 820 | if args.print_json: 821 | g.print_json = True 822 | 823 | if args.maxos: 824 | try: 825 | version = g.macos_to_num(args.maxos) 826 | if version: g.current_macos = version 827 | except: 828 | pass 829 | if args.catalog: 830 | # Set the catalog 831 | g.set_catalog(args.catalog) 832 | 833 | try: 834 | # Done setting up pre-requisites 835 | g.set_prods() 836 | 837 | if args.latest: 838 | g.get_latest(device_id=args.device_id, dmg=args.dmg) 839 | elif args.product != None: 840 | g.get_for_product(args.product, args.dmg) 841 | elif args.version != None: 842 | g.get_for_version(args.version, args.build, device_id=args.device_id, dmg=args.dmg) 843 | elif g.interactive: 844 | while True: 845 | try: 846 | g.main(args.dmg) 847 | except ProgramError as e: 848 | g.u.head(e.title) 849 | print(str(e)) 850 | print("") 851 | g.u.grab("Press [enter] to return...") 852 | else: 853 | raise ProgramError("No command specified") 854 | except ProgramError as e: 855 | print(str(e)) 856 | if g.interactive: 857 | print("") 858 | g.u.grab("Press [enter] to exit...") 859 | else: 860 | exit(1) 861 | exit(0) 862 | --------------------------------------------------------------------------------