├── .gitignore ├── BuildmacOSInstallApp.command ├── 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 /.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 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 | missing_list = [x for x in self.target_files if not x.lower() in lower_contents] 85 | if len(missing_list): 86 | self.u.head("Missing Required Files") 87 | print("") 88 | print("That folder is missing the following required files:") 89 | print(", ".join(missing_list)) 90 | print("") 91 | self.u.grab("Press [enter] to return...") 92 | # Time to build the installer! 93 | cwd = os.getcwd() 94 | os.chdir(f_path) 95 | base_mounts = [] 96 | try: 97 | self.u.head("Building Installer") 98 | print("") 99 | print("Taking ownership of downloaded files...") 100 | for x in self.target_files: 101 | print(" - {}...".format(x)) 102 | self.r.run({"args":["chmod","a+x",x]}) 103 | print("Mounting BaseSystem.dmg...") 104 | base_mounts = self.mount_dmg("BaseSystem.dmg") 105 | if not len(base_mounts): 106 | raise Exception("Mount Failed!", "No mount points were returned from BaseSystem.dmg") 107 | base_mount = base_mounts[0] # Let's assume the first 108 | print("Locating Installer app...") 109 | 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) 110 | if not install_app: 111 | raise Exception("Installer app not located in {}".format(base_mount)) 112 | print(" - Found {}".format(install_app)) 113 | # Copy the .app over 114 | out = self.r.run({"args":["cp","-R",os.path.join(base_mount,install_app),os.path.join(f_path,install_app)]}) 115 | if out[2] != 0: 116 | raise Exception("Copy Failed!", out[1]) 117 | print("Unmounting BaseSystem.dmg...") 118 | for x in base_mounts: 119 | self.unmount_dmg(x) 120 | base_mounts = [] 121 | shared_support = os.path.join(f_path,install_app,"Contents","SharedSupport") 122 | if not os.path.exists(shared_support): 123 | print("Creating SharedSupport directory...") 124 | os.makedirs(shared_support) 125 | print("Copying files to SharedSupport...") 126 | for x in self.target_files: 127 | y = "InstallESD.dmg" if x.lower() == "installesddmg.pkg" else x # InstallESDDmg.pkg gets renamed to InstallESD.dmg - all others stay the same 128 | print(" - {}{}".format(x, " --> {}".format(y) if y != x else "")) 129 | out = self.r.run({"args":["cp","-R",os.path.join(f_path,x),os.path.join(shared_support,y)]}) 130 | if out[2] != 0: 131 | raise Exception("Copy Failed!", out[1]) 132 | print("Patching InstallInfo.plist...") 133 | with open(os.path.join(shared_support,"InstallInfo.plist"),"rb") as f: 134 | p = plist.load(f) 135 | if "Payload Image Info" in p: 136 | pii = p["Payload Image Info"] 137 | if "URL" in pii: pii["URL"] = pii["URL"].replace("InstallESDDmg.pkg","InstallESD.dmg") 138 | if "id" in pii: pii["id"] = pii["id"].replace("com.apple.pkg.InstallESDDmg","com.apple.dmg.InstallESD") 139 | pii.pop("chunklistURL",None) 140 | pii.pop("chunklistid",None) 141 | with open(os.path.join(shared_support,"InstallInfo.plist"),"wb") as f: 142 | plist.dump(p,f) 143 | print("") 144 | print("Created: {}".format(install_app)) 145 | print("Saved to: {}".format(os.path.join(f_path,install_app))) 146 | print("") 147 | self.u.grab("Press [enter] to return...") 148 | except Exception as e: 149 | print("An error occurred:") 150 | print(" - {}".format(e)) 151 | print("") 152 | if len(base_mounts): 153 | for x in base_mounts: 154 | print(" - Unmounting {}...".format(x)) 155 | self.unmount_dmg(x) 156 | print("") 157 | self.u.grab("Press [enter] to return...") 158 | 159 | if __name__ == '__main__': 160 | b = buildMacOSInstallApp() 161 | b.main() 162 | -------------------------------------------------------------------------------- /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 | setlocal enableDelayedExpansion 3 | 4 | REM Setup initial vars 5 | set "script_name=" 6 | set "thisDir=%~dp0" 7 | set /a tried=0 8 | set "toask=yes" 9 | set "pause_on_error=yes" 10 | set "py2v=" 11 | set "py2path=" 12 | set "py3v=" 13 | set "py3path=" 14 | set "pypath=" 15 | 16 | REM use_py3: 17 | REM TRUE = Use if found, use py2 otherwise 18 | REM FALSE = Use py2 19 | REM FORCE = Use py3 20 | set "use_py3=TRUE" 21 | 22 | REM Get the system32 (or equivalent) path 23 | call :getsyspath "syspath" 24 | 25 | goto checkscript 26 | 27 | :checkscript 28 | REM Check for our script first 29 | set "looking_for=!script_name!" 30 | if "!script_name!" == "" ( 31 | set "looking_for=%~n0.py or %~n0.command" 32 | set "script_name=%~n0.py" 33 | if not exist "!thisDir!\!script_name!" ( 34 | set "script_name=%~n0.command" 35 | ) 36 | ) 37 | if not exist "!thisDir!\!script_name!" ( 38 | echo Could not find !looking_for!. 39 | echo Please make sure to run this script from the same directory 40 | echo as !looking_for!. 41 | echo. 42 | echo Press [enter] to quit. 43 | pause > nul 44 | exit /b 45 | ) 46 | goto checkpy 47 | 48 | :getsyspath 49 | REM Helper method to return the "proper" path to cmd.exe, reg.exe, and where.exe by walking the ComSpec var 50 | REM Prep the LF variable to use the "line feed" approach 51 | (SET LF=^ 52 | %=this line is empty=% 53 | ) 54 | REM Strip double semi-colons 55 | call :undouble "ComSpec" "%ComSpec%" ";" 56 | set "testpath=%ComSpec:;=!LF!%" 57 | REM Let's walk each path and test if cmd.exe, reg.exe, and where.exe exist there 58 | set /a found=0 59 | for /f "tokens=* delims=" %%i in ("!testpath!") do ( 60 | REM Only continue if we haven't found it yet 61 | if NOT "%%i" == "" ( 62 | if !found! lss 1 ( 63 | set "temppath=%%i" 64 | REM Remove "cmd.exe" from the end if it exists 65 | if /i "!temppath:~-7!" == "cmd.exe" ( 66 | set "temppath=!temppath:~0,-7!" 67 | ) 68 | REM Pad the end with a backslash if needed 69 | if NOT "!temppath:~-1!" == "\" ( 70 | set "temppath=!temppath!\" 71 | ) 72 | REM Let's see if cmd, reg, and where exist there - and set it if so 73 | if EXIST "!temppath!cmd.exe" ( 74 | if EXIST "!temppath!reg.exe" ( 75 | if EXIST "!temppath!where.exe" ( 76 | set /a found=1 77 | set "ComSpec=!temppath!cmd.exe" 78 | set "%~1=!temppath!" 79 | ) 80 | ) 81 | ) 82 | ) 83 | ) 84 | ) 85 | goto :EOF 86 | 87 | :updatepath 88 | set "spath=" 89 | set "upath=" 90 | 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" ) 91 | 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" ) 92 | if not "%spath%" == "" ( 93 | REM We got something in the system path 94 | set "PATH=%spath%" 95 | if not "!upath!" == "" ( 96 | REM We also have something in the user path 97 | set "PATH=%PATH%;%upath%" 98 | ) 99 | ) else if not "%upath%" == "" ( 100 | set "PATH=%upath%" 101 | ) 102 | REM Remove double semicolons from the adjusted PATH 103 | call :undouble "PATH" "%PATH%" ";" 104 | goto :EOF 105 | 106 | :undouble 107 | REM Helper function to strip doubles of a single character out of a string recursively 108 | set "string_value=%~2" 109 | set "check=!string_value:%~3%~3=%~3!" 110 | if not "!check!" == "!string_value!" ( 111 | set "%~1=!check!" 112 | call :undouble "%~1" "!check!" "%~3" 113 | ) 114 | goto :EOF 115 | 116 | :checkpy 117 | call :updatepath 118 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe python 2^> nul`) do ( call :checkpyversion "%%x" "py2v" "py2path" "py3v" "py3path" ) 119 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe python3 2^> nul`) do ( call :checkpyversion "%%x" "py2v" "py2path" "py3v" "py3path" ) 120 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe py 2^> nul`) do ( call :checkpylauncher "%%x" "py2v" "py2path" "py3v" "py3path" ) 121 | set "targetpy=3" 122 | if /i "!use_py3!" == "FALSE" ( 123 | set "targetpy=2" 124 | set "pypath=!py2path!" 125 | ) else if /i "!use_py3!" == "FORCE" ( 126 | set "pypath=!py3path!" 127 | ) else if /i "!use_py3!" == "TRUE" ( 128 | set "pypath=!py3path!" 129 | if "!pypath!" == "" set "pypath=!py2path!" 130 | ) 131 | if not "!pypath!" == "" ( 132 | goto runscript 133 | ) 134 | if !tried! lss 1 ( 135 | if /i "!toask!"=="yes" ( 136 | REM Better ask permission first 137 | goto askinstall 138 | ) else ( 139 | goto installpy 140 | ) 141 | ) else ( 142 | cls 143 | echo ### ### 144 | echo # Warning # 145 | echo ### ### 146 | echo. 147 | REM Couldn't install for whatever reason - give the error message 148 | echo Python is not installed or not found in your PATH var. 149 | echo Please install it from https://www.python.org/downloads/windows/ 150 | echo. 151 | echo Make sure you check the box labeled: 152 | echo. 153 | echo "Add Python X.X to PATH" 154 | echo. 155 | echo Where X.X is the py version you're installing. 156 | echo. 157 | echo Press [enter] to quit. 158 | pause > nul 159 | exit /b 160 | ) 161 | goto runscript 162 | 163 | :checkpylauncher 164 | REM Attempt to check the latest python 2 and 3 versions via the py launcher 165 | for /f "USEBACKQ tokens=*" %%x in (`%~1 -2 -c "import sys; print(sys.executable)" 2^> nul`) do ( call :checkpyversion "%%x" "%~2" "%~3" "%~4" "%~5" ) 166 | for /f "USEBACKQ tokens=*" %%x in (`%~1 -3 -c "import sys; print(sys.executable)" 2^> nul`) do ( call :checkpyversion "%%x" "%~2" "%~3" "%~4" "%~5" ) 167 | goto :EOF 168 | 169 | :checkpyversion 170 | set "version="&for /f "tokens=2* USEBACKQ delims= " %%a in (`"%~1" -V 2^>^&1`) do ( 171 | REM Ensure we have a version number 172 | call :isnumber "%%a" 173 | if not "!errorlevel!" == "0" goto :EOF 174 | set "version=%%a" 175 | ) 176 | if not defined version goto :EOF 177 | if "!version:~0,1!" == "2" ( 178 | REM Python 2 179 | call :comparepyversion "!version!" "!%~2!" 180 | if "!errorlevel!" == "1" ( 181 | set "%~2=!version!" 182 | set "%~3=%~1" 183 | ) 184 | ) else ( 185 | REM Python 3 186 | call :comparepyversion "!version!" "!%~4!" 187 | if "!errorlevel!" == "1" ( 188 | set "%~4=!version!" 189 | set "%~5=%~1" 190 | ) 191 | ) 192 | goto :EOF 193 | 194 | :isnumber 195 | set "var="&for /f "delims=0123456789." %%i in ("%~1") do set var=%%i 196 | if defined var (exit /b 1) 197 | exit /b 0 198 | 199 | :comparepyversion 200 | REM Exits with status 0 if equal, 1 if v1 gtr v2, 2 if v1 lss v2 201 | for /f "tokens=1,2,3 delims=." %%a in ("%~1") do ( 202 | set a1=%%a 203 | set a2=%%b 204 | set a3=%%c 205 | ) 206 | for /f "tokens=1,2,3 delims=." %%a in ("%~2") do ( 207 | set b1=%%a 208 | set b2=%%b 209 | set b3=%%c 210 | ) 211 | if not defined a1 set a1=0 212 | if not defined a2 set a2=0 213 | if not defined a3 set a3=0 214 | if not defined b1 set b1=0 215 | if not defined b2 set b2=0 216 | if not defined b3 set b3=0 217 | if %a1% gtr %b1% exit /b 1 218 | if %a1% lss %b1% exit /b 2 219 | if %a2% gtr %b2% exit /b 1 220 | if %a2% lss %b2% exit /b 2 221 | if %a3% gtr %b3% exit /b 1 222 | if %a3% lss %b3% exit /b 2 223 | exit /b 0 224 | 225 | :askinstall 226 | cls 227 | echo ### ### 228 | echo # Python Not Found # 229 | echo ### ### 230 | echo. 231 | echo Python !targetpy! was not found on the system or in the PATH var. 232 | echo. 233 | set /p "menu=Would you like to install it now? [y/n]: " 234 | if /i "!menu!"=="y" ( 235 | REM We got the OK - install it 236 | goto installpy 237 | ) else if "!menu!"=="n" ( 238 | REM No OK here... 239 | set /a tried=!tried!+1 240 | goto checkpy 241 | ) 242 | REM Incorrect answer - go back 243 | goto askinstall 244 | 245 | :installpy 246 | REM This will attempt to download and install python 247 | REM First we get the html for the python downloads page for Windows 248 | set /a tried=!tried!+1 249 | cls 250 | echo ### ### 251 | echo # Installing Python # 252 | echo ### ### 253 | echo. 254 | echo Gathering info from https://www.python.org/downloads/windows/... 255 | powershell -command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (new-object System.Net.WebClient).DownloadFile('https://www.python.org/downloads/windows/','%TEMP%\pyurl.txt')" 256 | if not exist "%TEMP%\pyurl.txt" ( 257 | goto checkpy 258 | ) 259 | 260 | echo Parsing for latest... 261 | pushd "%TEMP%" 262 | :: Version detection code slimmed by LussacZheng (https://github.com/corpnewt/gibMacOS/issues/20) 263 | for /f "tokens=9 delims=< " %%x in ('findstr /i /c:"Latest Python !targetpy! Release" pyurl.txt') do ( set "release=%%x" ) 264 | popd 265 | 266 | echo Found Python !release! - Downloading... 267 | REM Let's delete our txt file now - we no longer need it 268 | del "%TEMP%\pyurl.txt" 269 | 270 | REM At this point - we should have the version number. 271 | REM We can build the url like so: "https://www.python.org/ftp/python/[version]/python-[version]-amd64.exe" 272 | set "url=https://www.python.org/ftp/python/!release!/python-!release!-amd64.exe" 273 | set "pytype=exe" 274 | if "!targetpy!" == "2" ( 275 | set "url=https://www.python.org/ftp/python/!release!/python-!release!.amd64.msi" 276 | set "pytype=msi" 277 | ) 278 | REM Now we download it with our slick powershell command 279 | powershell -command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (new-object System.Net.WebClient).DownloadFile('!url!','%TEMP%\pyinstall.!pytype!')" 280 | REM If it doesn't exist - we bail 281 | if not exist "%TEMP%\pyinstall.!pytype!" ( 282 | goto checkpy 283 | ) 284 | REM It should exist at this point - let's run it to install silently 285 | echo Installing... 286 | pushd "%TEMP%" 287 | if /i "!pytype!" == "exe" ( 288 | echo pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 289 | pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 290 | ) else ( 291 | set "foldername=!release:.=!" 292 | echo msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR="%LocalAppData%\Programs\Python\Python!foldername:~0,2!" 293 | msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR="%LocalAppData%\Programs\Python\Python!foldername:~0,2!" 294 | ) 295 | popd 296 | echo Installer finished with %ERRORLEVEL% status. 297 | REM Now we should be able to delete the installer and check for py again 298 | del "%TEMP%\pyinstall.!pytype!" 299 | REM If it worked, then we should have python in our PATH 300 | REM this does not get updated right away though - let's try 301 | REM manually updating the local PATH var 302 | call :updatepath 303 | goto checkpy 304 | exit /b 305 | 306 | :runscript 307 | REM Python found 308 | cls 309 | set "args=%*" 310 | set "args=!args:"=!" 311 | if "!args!"=="" ( 312 | "!pypath!" "!thisDir!!script_name!" 313 | ) else ( 314 | "!pypath!" "!thisDir!!script_name!" %* 315 | ) 316 | if /i "!pause_on_error!" == "yes" ( 317 | if not "%ERRORLEVEL%" == "0" ( 318 | echo. 319 | echo Script exited with error code: %ERRORLEVEL% 320 | echo. 321 | echo Press [enter] to exit... 322 | pause > nul 323 | ) 324 | ) 325 | goto :EOF 326 | -------------------------------------------------------------------------------- /MakeInstall.py: -------------------------------------------------------------------------------- 1 | from Scripts import utils, diskwin, downloader, run 2 | import os, sys, tempfile, shutil, zipfile, platform, json, time 3 | 4 | class WinUSB: 5 | 6 | def __init__(self): 7 | self.u = utils.Utils("MakeInstall") 8 | if not self.u.check_admin(): 9 | # Try to self-elevate 10 | self.u.elevate(os.path.realpath(__file__)) 11 | exit() 12 | self.min_plat = 9600 13 | # Make sure we're on windows 14 | self.verify_os() 15 | # Setup initial vars 16 | self.d = diskwin.Disk() 17 | self.dl = downloader.Downloader() 18 | self.r = run.Run() 19 | self.scripts = "Scripts" 20 | self.s_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), self.scripts) 21 | # self.dd_url = "http://www.chrysocome.net/downloads/ddrelease64.exe" 22 | self.dd_url = "https://github.com/corpnewt/gibMacOS/files/4573241/ddrelease64.exe.zip" # Rehost due to download issues 23 | self.dd_name = ".".join(os.path.basename(self.dd_url).split(".")[:-1]) # Get the name without the last extension 24 | self.z_json = "https://sourceforge.net/projects/sevenzip/best_release.json" 25 | self.z_url2 = "https://www.7-zip.org/a/7z1806-x64.msi" 26 | self.z_url = "https://www.7-zip.org/a/7z[[vers]]-x64.msi" 27 | self.z_name = "7z.exe" 28 | self.bi_url = "https://raw.githubusercontent.com/corpnewt/gibMacOS/master/Scripts/BOOTICEx64.exe" 29 | self.bi_name = "BOOTICEx64.exe" 30 | self.clover_url = "https://api.github.com/repos/CloverHackyColor/CloverBootloader/releases" 31 | self.dids_url = "https://api.github.com/repos/dids/clover-builder/releases" 32 | self.oc_url = "https://api.github.com/repos/acidanthera/OpenCorePkg/releases" 33 | self.oc_boot = "boot" 34 | self.oc_boot0 = "boot0" 35 | self.oc_boot1 = "boot1f32" 36 | # self.oc_boot_url = "https://github.com/acidanthera/OpenCorePkg/raw/master/Utilities/LegacyBoot/" 37 | self.oc_boot_url = "https://github.com/acidanthera/OpenCorePkg/raw/870017d0e5d53abeaf0347997da912c3e382a04a/Utilities/LegacyBoot/" 38 | self.diskpart = os.path.join(os.environ['SYSTEMDRIVE'] + "\\", "Windows", "System32", "diskpart.exe") 39 | # From Tim Sutton's brigadier: https://github.com/timsutton/brigadier/blob/master/brigadier 40 | self.z_path = None 41 | self.z_path64 = os.path.join(os.environ['SYSTEMDRIVE'] + "\\", "Program Files", "7-Zip", "7z.exe") 42 | self.z_path32 = os.path.join(os.environ['SYSTEMDRIVE'] + "\\", "Program Files (x86)", "7-Zip", "7z.exe") 43 | self.recovery_suffixes = ( 44 | "recoveryhdupdate.pkg", 45 | "recoveryhdmetadmg.pkg" 46 | ) 47 | self.dd_bootsector = True 48 | self.boot0 = "boot0af" 49 | self.boot1 = "boot1f32alt" 50 | self.boot = "boot6" 51 | self.efi_id = "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" # EFI 52 | self.bas_id = "ebd0a0a2-b9e5-4433-87c0-68b6b72699c7" # Microsoft Basic Data 53 | self.hfs_id = "48465300-0000-11AA-AA11-00306543ECAC" # HFS+ 54 | self.rec_id = "426F6F74-0000-11AA-AA11-00306543ECAC" # Apple Boot partition (Recovery HD) 55 | self.show_all_disks = False 56 | 57 | def verify_os(self): 58 | self.u.head("Verifying OS") 59 | print("") 60 | print("Verifying OS name...") 61 | if not os.name=="nt": 62 | print("") 63 | print("This script is only for Windows!") 64 | print("") 65 | self.u.grab("Press [enter] to exit...") 66 | exit(1) 67 | print(" - Name = NT") 68 | print("Verifying OS version...") 69 | # Verify we're at version 9600 or greater 70 | try: 71 | # Set plat to the last item of the output split by . - looks like: 72 | # Windows-8.1-6.3.9600 73 | # or this: 74 | # Windows-10-10.0.17134-SP0 75 | plat = int(platform.platform().split(".")[-1].split("-")[0]) 76 | except: 77 | plat = 0 78 | if plat < self.min_plat: 79 | print("") 80 | print("Currently running {}, this script requires version {} or newer.".format(platform.platform(), self.min_plat)) 81 | print("") 82 | self.u.grab("Press [enter] to exit...") 83 | exit(1) 84 | print(" - Version = {}".format(plat)) 85 | print("") 86 | print("{} >= {}, continuing...".format(plat, self.min_plat)) 87 | 88 | def get_disks_of_type(self, disk_list, disk_type=(0,2)): 89 | disks = {} 90 | for disk in disk_list: 91 | if disk_list[disk].get("type",0) in disk_type: 92 | disks[disk] = disk_list[disk] 93 | return disks 94 | 95 | def check_dd(self): 96 | # Checks if ddrelease64.exe exists in our Scripts dir 97 | # and if not - downloads it 98 | # 99 | # Returns True if exists/downloaded successfully 100 | # or False if issues. 101 | # Check for dd.exe in the current dir 102 | if os.path.exists(os.path.join(self.s_path, self.dd_name)): 103 | # print("Located {}!".format(self.dd_name)) 104 | # Got it 105 | return True 106 | print("Couldn't locate {} - downloading...".format(self.dd_name)) 107 | temp = tempfile.mkdtemp() 108 | z_file = os.path.basename(self.dd_url) 109 | # Now we need to download 110 | self.dl.stream_to_file(self.dd_url, os.path.join(temp,z_file)) 111 | print(" - Extracting...") 112 | # Extract with built-in tools \o/ 113 | cwd = os.getcwd() 114 | os.chdir(temp) 115 | with zipfile.ZipFile(os.path.join(temp,z_file)) as z: 116 | z.extractall(temp) 117 | for x in os.listdir(temp): 118 | if self.dd_name.lower() == x.lower(): 119 | # Found it 120 | print(" - Found {}".format(x)) 121 | print(" - Copying to {} directory...".format(self.scripts)) 122 | shutil.copy(os.path.join(temp,x), os.path.join(self.s_path,x)) 123 | # Return to prior cwd 124 | os.chdir(cwd) 125 | # Remove the temp folder 126 | shutil.rmtree(temp,ignore_errors=True) 127 | print("") 128 | return os.path.exists(os.path.join(self.s_path, self.dd_name)) 129 | 130 | def check_7z(self): 131 | self.z_path = self.z_path64 if os.path.exists(self.z_path64) else self.z_path32 if os.path.exists(self.z_path32) else None 132 | if self.z_path: 133 | return True 134 | print("Didn't locate {} - downloading...".format(self.z_name)) 135 | # Didn't find it - let's do some stupid stuff 136 | # First we get our json response - or rather, try to, then parse it 137 | # looking for the current version 138 | dl_url = None 139 | try: 140 | json_data = json.loads(self.dl.get_string(self.z_json)) 141 | v_num = json_data.get("release",{}).get("filename","").split("/")[-1].lower().split("-")[0].replace("7z","").replace(".exe","") 142 | if len(v_num): 143 | dl_url = self.z_url.replace("[[vers]]",v_num) 144 | except: 145 | pass 146 | if not dl_url: 147 | dl_url = self.z_url2 148 | temp = tempfile.mkdtemp() 149 | dl_file = self.dl.stream_to_file(dl_url, os.path.join(temp, self.z_name)) 150 | if not dl_file: # Didn't download right 151 | shutil.rmtree(temp,ignore_errors=True) 152 | return False 153 | print("") 154 | print("Installing 7zip...") 155 | # From Tim Sutton's brigadier: https://github.com/timsutton/brigadier/blob/master/brigadier 156 | out = self.r.run({"args":["msiexec", "/qn", "/i", os.path.join(temp, self.z_name)],"stream":True}) 157 | if out[2] != 0: 158 | shutil.rmtree(temp,ignore_errors=True) 159 | print("Error ({})".format(out[2])) 160 | print("") 161 | self.u.grab("Press [enter] to exit...") 162 | exit(1) 163 | print("") 164 | self.z_path = self.z_path64 if os.path.exists(self.z_path64) else self.z_path32 if os.path.exists(self.z_path32) else None 165 | return self.z_path and os.path.exists(self.z_path) 166 | 167 | def check_bi(self): 168 | # Checks for BOOTICEx64.exe in our scripts dir 169 | # and downloads it if need be 170 | if os.path.exists(os.path.join(self.s_path, self.bi_name)): 171 | # print("Located {}!".format(self.bi_name)) 172 | # Got it 173 | return True 174 | print("Couldn't locate {} - downloading...".format(self.bi_name)) 175 | self.dl.stream_to_file(self.bi_url, os.path.join(self.s_path, self.bi_name)) 176 | print("") 177 | return os.path.exists(os.path.join(self.s_path,self.bi_name)) 178 | 179 | def get_dl_url_from_json(self,json_data,suffix=".lzma"): 180 | try: j_list = json.loads(json_data) 181 | except: return None 182 | j_list = j_list if isinstance(j_list,list) else [j_list] 183 | for j in j_list: 184 | dl_link = next((x.get("browser_download_url", None) for x in j.get("assets", []) if x.get("browser_download_url", "").lower().endswith(suffix.lower())), None) 185 | if dl_link: break 186 | if not dl_link: 187 | return None 188 | return { "url" : dl_link, "name" : os.path.basename(dl_link), "info" : j.get("body", None) } 189 | 190 | def get_dl_info(self,clover_version=None): 191 | # Returns the latest download package and info in a 192 | # dictionary: { "url" : dl_url, "name" : name, "info" : update_info } 193 | # Attempt Dids' repo first - falling back on Clover's official repo as needed 194 | for url in (self.dids_url,self.clover_url): 195 | # Tag is 5098 on Slice's repo, and v2.5k_r5098 on Dids' - accommodate as needed 196 | search_url = url if clover_version == None else "{}/tags/{}".format(url,clover_version if url == self.clover_url else "v2.{}k_r{}".format(clover_version[0],clover_version)) 197 | print(" - Checking {}".format(search_url)) 198 | json_data = self.dl.get_string(search_url, False) 199 | if not json_data: print(" --> Not found!") 200 | else: return self.get_dl_url_from_json(json_data) 201 | return None 202 | 203 | def get_oc_dl_info(self): 204 | json_data = self.dl.get_string(self.oc_url, False) 205 | if not json_data: print(" --> Not found!") 206 | else: return self.get_dl_url_from_json(json_data,"-RELEASE.zip") 207 | 208 | def diskpart_flag(self, disk, as_efi=False): 209 | # Sets and unsets the GUID needed for a GPT EFI partition ID 210 | self.u.head("Changing ID With DiskPart") 211 | print("") 212 | print("Setting type as {}...".format("EFI" if as_efi else "Basic Data")) 213 | print("") 214 | # - EFI system partition: c12a7328-f81f-11d2-ba4b-00a0c93ec93b 215 | # - Basic data partition: ebd0a0a2-b9e5-4433-87c0-68b6b72699c7 216 | dp_script = "\n".join([ 217 | "select disk {}".format(disk.get("index",-1)), 218 | "sel part 1", 219 | "set id={}".format(self.efi_id if as_efi else self.bas_id) 220 | ]) 221 | temp = tempfile.mkdtemp() 222 | script = os.path.join(temp, "diskpart.txt") 223 | try: 224 | with open(script,"w") as f: 225 | f.write(dp_script) 226 | except: 227 | shutil.rmtree(temp) 228 | print("Error creating script!") 229 | print("") 230 | self.u.grab("Press [enter] to return...") 231 | return 232 | # Let's try to run it! 233 | out = self.r.run({"args":[self.diskpart,"/s",script],"stream":True}) 234 | # Ditch our script regardless of whether diskpart worked or not 235 | shutil.rmtree(temp) 236 | print("") 237 | if out[2] != 0: 238 | # Error city! 239 | print("DiskPart exited with non-zero status ({}). Aborting.".format(out[2])) 240 | else: 241 | print("Done - You may need to replug your drive for the") 242 | print("changes to take effect.") 243 | print("") 244 | self.u.grab("Press [enter] to return...") 245 | 246 | def diskpart_erase(self, disk, gpt=False, clover_version = None): 247 | # Generate a script that we can pipe to diskpart to erase our disk 248 | self.u.head("Erasing With DiskPart") 249 | print("") 250 | # Then we'll re-gather our disk info on success and move forward 251 | # Using MBR to effectively set the individual partition types 252 | # Keeps us from having issues mounting the EFI on Windows - 253 | # and also lets us explicitly set the partition id for the main 254 | # data partition. 255 | if not gpt: 256 | print("Using MBR...") 257 | dp_script = "\n".join([ 258 | "select disk {}".format(disk.get("index",-1)), 259 | "clean", 260 | "convert mbr", 261 | "create partition primary size=200", 262 | "format quick fs=fat32 label='BOOT'", 263 | "active", 264 | "create partition primary", 265 | "select part 2", 266 | "set id=AB", # AF = HFS, AB = Recovery 267 | "select part 1", 268 | "assign" 269 | ]) 270 | else: 271 | print("Using GPT...") 272 | dp_script = "\n".join([ 273 | "select disk {}".format(disk.get("index",-1)), 274 | "clean", 275 | "convert gpt", 276 | "create partition primary size=200", 277 | "format quick fs=fat32 label='BOOT'", 278 | "create partition primary id={}".format(self.hfs_id) 279 | ]) 280 | temp = tempfile.mkdtemp() 281 | script = os.path.join(temp, "diskpart.txt") 282 | try: 283 | with open(script,"w") as f: 284 | f.write(dp_script) 285 | except: 286 | shutil.rmtree(temp) 287 | print("Error creating script!") 288 | print("") 289 | self.u.grab("Press [enter] to return...") 290 | return 291 | # Let's try to run it! 292 | out = self.r.run({"args":[self.diskpart,"/s",script],"stream":True}) 293 | # Ditch our script regardless of whether diskpart worked or not 294 | shutil.rmtree(temp) 295 | if out[2] != 0: 296 | # Error city! 297 | print("") 298 | print("DiskPart exited with non-zero status ({}). Aborting.".format(out[2])) 299 | print("") 300 | self.u.grab("Press [enter] to return...") 301 | return 302 | # We should now have a fresh drive to work with 303 | # Let's write an image or something 304 | self.u.head("Updating Disk Information") 305 | print("") 306 | print("Re-populating list...") 307 | self.d.update() 308 | print("Relocating disk {}".format(disk["index"])) 309 | disk = self.d.disks[str(disk["index"])] 310 | self.select_package(disk, clover_version) 311 | 312 | def select_package(self, disk, clover_version = None): 313 | self.u.head("Select Recovery Package") 314 | print("") 315 | print("{}. {} - {} ({})".format( 316 | disk.get("index",-1), 317 | disk.get("model","Unknown"), 318 | self.dl.get_size(disk.get("size",-1),strip_zeroes=True), 319 | ["Unknown","No Root Dir","Removable","Local","Network","Disc","RAM Disk"][disk.get("type",0)] 320 | )) 321 | print("") 322 | print("M. Main Menu") 323 | print("Q. Quit") 324 | print("") 325 | menu = self.u.grab("Please paste the recovery update pkg path to extract: ") 326 | if menu.lower() == "q": 327 | self.u.custom_quit() 328 | if menu.lower() == "m": 329 | return 330 | path = self.u.check_path(menu) 331 | if not path: 332 | self.select_package(disk, clover_version) 333 | return 334 | # Got the package - let's make sure it's named right - just in case 335 | if os.path.basename(path).lower().endswith(".hfs"): 336 | # We have an hfs image already - bypass extraction 337 | self.dd_image(disk, path, clover_version) 338 | return 339 | # If it's a directory, find the first recovery hit 340 | if os.path.isdir(path): 341 | for f in os.listdir(path): 342 | if f.lower().endswith(self.recovery_suffixes): 343 | path = os.path.join(path, f) 344 | break 345 | # Make sure it's named right for recovery stuffs 346 | if not path.lower().endswith(self.recovery_suffixes): 347 | self.u.head("Invalid Package") 348 | print("") 349 | print("{} is not in the available recovery package names:\n{}".format(os.path.basename(path), ", ".join(self.recovery_suffixes))) 350 | print("") 351 | print("Ensure you're passing a proper recovery package.") 352 | print("") 353 | self.u.grab("Press [enter] to return to package selection...") 354 | self.select_package(disk, clover_version) 355 | return 356 | self.u.head("Extracting Package") 357 | print("") 358 | temp = tempfile.mkdtemp() 359 | cwd = os.getcwd() 360 | os.chdir(temp) 361 | # Extract in sections and remove any files we run into 362 | print("Extracting Recovery dmg...") 363 | out = self.r.run({"args":[self.z_path, "e", "-txar", path, "*.dmg"]}) 364 | if out[2] != 0: 365 | shutil.rmtree(temp,ignore_errors=True) 366 | print("An error occurred extracting: {}".format(out[2])) 367 | print("") 368 | self.u.grab("Press [enter] to return...") 369 | return 370 | print("Extracting BaseSystem.dmg...") 371 | # No files to delete here - let's extract the next part 372 | out = self.r.run({"args":[self.z_path, "e", "*.dmg", "*/Base*.dmg"]}) 373 | if out[2] != 0: 374 | shutil.rmtree(temp,ignore_errors=True) 375 | print("An error occurred extracting: {}".format(out[2])) 376 | print("") 377 | self.u.grab("Press [enter] to return...") 378 | return 379 | # If we got here - we should delete everything in the temp folder except 380 | # for a .dmg that starts with Base 381 | del_list = [x for x in os.listdir(temp) if not (x.lower().startswith("base") and x.lower().endswith(".dmg"))] 382 | for d in del_list: 383 | os.remove(os.path.join(temp, d)) 384 | # Onto the last command 385 | print("Extracting hfs...") 386 | out = self.r.run({"args":[self.z_path, "e", "-tdmg", "Base*.dmg", "*.hfs"]}) 387 | if out[2] != 0: 388 | shutil.rmtree(temp,ignore_errors=True) 389 | print("An error occurred extracting: {}".format(out[2])) 390 | print("") 391 | self.u.grab("Press [enter] to return...") 392 | return 393 | # If we got here - we should delete everything in the temp folder except 394 | # for a .dmg that starts with Base 395 | del_list = [x for x in os.listdir(temp) if not x.lower().endswith(".hfs")] 396 | for d in del_list: 397 | os.remove(os.path.join(temp, d)) 398 | print("Extracted successfully!") 399 | hfs = next((x for x in os.listdir(temp) if x.lower().endswith(".hfs")),None) 400 | # Now to dd our image - if it exists 401 | if not hfs: 402 | print("Missing the .hfs file! Aborting.") 403 | print("") 404 | self.u.grab("Press [enter] to return...") 405 | else: 406 | self.dd_image(disk, os.path.join(temp, hfs), clover_version) 407 | shutil.rmtree(temp,ignore_errors=True) 408 | 409 | def dd_image(self, disk, image, clover_version = None): 410 | # Let's dd the shit out of our disk 411 | self.u.head("Copying Image To Drive") 412 | print("") 413 | print("Image: {}".format(image)) 414 | print("") 415 | print("Disk {}. {} - {} ({})".format( 416 | disk.get("index",-1), 417 | disk.get("model","Unknown"), 418 | self.dl.get_size(disk.get("size",-1),strip_zeroes=True), 419 | ["Unknown","No Root Dir","Removable","Local","Network","Disc","RAM Disk"][disk.get("type",0)] 420 | )) 421 | print("") 422 | args = [ 423 | os.path.join(self.s_path, self.dd_name), 424 | "if={}".format(image), 425 | "of=\\\\?\\Device\Harddisk{}\Partition2".format(disk.get("index",-1)), 426 | "bs=8M", 427 | "--progress" 428 | ] 429 | print(" ".join(args)) 430 | print("") 431 | print("This may take some time!") 432 | print("") 433 | out = self.r.run({"args":args}) 434 | if len(out[1].split("Error")) > 1: 435 | # We had some error text - dd, even when failing likes to give us a 0 436 | # status code. It also sends a ton of text through stderr - so we comb 437 | # that for "Error" then split by that to skip the extra fluff and show only 438 | # the error. 439 | print("An error occurred:\n\n{}".format("Error"+out[1].split("Error")[1])) 440 | print("") 441 | self.u.grab("Press [enter] to return to the main menu...") 442 | return 443 | # Install Clover/OC to the target drive 444 | if clover_version == "OpenCore": self.install_oc(disk) 445 | else: self.install_clover(disk, clover_version) 446 | 447 | def install_oc(self, disk): 448 | self.u.head("Installing OpenCore") 449 | print("") 450 | print("Gathering info...") 451 | o = self.get_oc_dl_info() 452 | if o == None: 453 | print(" - Error communicating with github!") 454 | print("") 455 | self.u.grab("Press [enter] to return...") 456 | return 457 | print(" - Got {}".format(o.get("name","Unknown Version"))) 458 | print("Downloading...") 459 | temp = tempfile.mkdtemp() 460 | os.chdir(temp) 461 | oc_zip = o["name"] 462 | self.dl.stream_to_file(o["url"], os.path.join(temp, o["name"])) 463 | print("") # Empty space to clear the download progress 464 | if not os.path.exists(os.path.join(temp, o["name"])): 465 | shutil.rmtree(temp,ignore_errors=True) 466 | print(" - Download failed. Aborting...") 467 | print("") 468 | self.u.grab("Press [enter] to return...") 469 | return 470 | # Got a valid file in our temp dir 471 | print("Extracting {}...".format(oc_zip)) 472 | out = self.r.run({"args":[self.z_path, "x", os.path.join(temp,oc_zip)]}) 473 | if out[2] != 0: 474 | shutil.rmtree(temp,ignore_errors=True) 475 | print(" - An error occurred extracting: {}".format(out[2])) 476 | print("") 477 | self.u.grab("Press [enter] to return...") 478 | return 479 | # We need to also gather our boot, boot0af, and boot1f32 files 480 | print("Gathering DUET boot files...") 481 | for x in (self.oc_boot,self.oc_boot0,self.oc_boot1): 482 | print(" - {}".format(x)) 483 | self.dl.stream_to_file(self.oc_boot_url + x, os.path.join(temp,x),False) 484 | # At this point, we should have a boot0xx file and an EFI folder in the temp dir 485 | # We need to udpate the disk list though - to reflect the current file system on part 1 486 | # of our current disk 487 | self.d.update() # assumes our disk number stays the same 488 | # Some users are having issues with the "partitions" key not populating - possibly a 3rd party disk management soft? 489 | # Possibly a bad USB? 490 | # We'll see if the key exists - if not, we'll throw an error. 491 | if self.d.disks[str(disk["index"])].get("partitions",None) == None: 492 | # No partitions found. 493 | shutil.rmtree(temp,ignore_errors=True) 494 | print("No partitions located on disk!") 495 | print("") 496 | self.u.grab("Press [enter] to return...") 497 | return 498 | part = self.d.disks[str(disk["index"])]["partitions"].get("0",{}).get("letter",None) # get the first partition's letter 499 | if part == None: 500 | shutil.rmtree(temp,ignore_errors=True) 501 | print("Lost original disk - or formatting failed!") 502 | print("") 503 | self.u.grab("Press [enter] to return...") 504 | return 505 | # Here we have our disk and partitions and such - the BOOT partition 506 | # will be the first partition 507 | # Let's copy over the EFI folder and then dd the boot0xx file 508 | print("Copying EFI folder to {}/EFI...".format(part)) 509 | if os.path.exists("{}/EFI".format(part)): 510 | print(" - EFI exists - removing...") 511 | shutil.rmtree("{}/EFI".format(part),ignore_errors=True) 512 | time.sleep(1) # Added because windows is dumb 513 | shutil.copytree(os.path.join(temp,"X64","EFI"), "{}/EFI".format(part)) 514 | # Copy boot over to the root of the EFI volume 515 | print("Copying {} to {}/boot...".format(self.oc_boot,part)) 516 | shutil.copy(os.path.join(temp,self.oc_boot),"{}/boot".format(part)) 517 | # Use bootice to update the MBR and PBR - always on the first 518 | # partition (which is 0 in bootice) 519 | print("Updating the MBR with {}...".format(self.oc_boot0)) 520 | args = [ 521 | os.path.join(self.s_path,self.bi_name), 522 | "/device={}".format(disk.get("index",-1)), 523 | "/mbr", 524 | "/restore", 525 | "/file={}".format(os.path.join(temp,self.oc_boot0)), 526 | "/keep_dpt", 527 | "/quiet" 528 | ] 529 | out = self.r.run({"args":args}) 530 | if out[2] != 0: 531 | shutil.rmtree(temp,ignore_errors=True) 532 | print(" - An error occurred updating the MBR: {}".format(out[2])) 533 | print("") 534 | self.u.grab("Press [enter] to return...") 535 | return 536 | print("Updating the PBR with {}...".format(self.oc_boot1)) 537 | args = [ 538 | os.path.join(self.s_path,self.bi_name), 539 | "/device={}:0".format(disk.get("index",-1)), 540 | "/pbr", 541 | "/restore", 542 | "/file={}".format(os.path.join(temp,self.oc_boot1)), 543 | "/keep_bpb", 544 | "/quiet" 545 | ] 546 | out = self.r.run({"args":args}) 547 | if out[2] != 0: 548 | shutil.rmtree(temp,ignore_errors=True) 549 | print(" - An error occurred updating the PBR: {}".format(out[2])) 550 | print("") 551 | self.u.grab("Press [enter] to return...") 552 | return 553 | print("Cleaning up...") 554 | shutil.rmtree(temp,ignore_errors=True) 555 | print("") 556 | print("Done.") 557 | print("") 558 | self.u.grab("Press [enter] to return to the main menu...") 559 | 560 | def install_clover(self, disk, clover_version = None): 561 | self.u.head("Installing Clover - {}".format("Latest" if not clover_version else "r"+clover_version)) 562 | print("") 563 | print("Gathering info...") 564 | c = self.get_dl_info(clover_version) 565 | if c == None: 566 | if clover_version == None: print(" - Error communicating with github!") 567 | else: print(" - Error gathering info for Clover r{}".format(clover_version)) 568 | print("") 569 | self.u.grab("Press [enter] to return...") 570 | return 571 | print(" - Got {}".format(c.get("name","Unknown Version"))) 572 | print("Downloading...") 573 | temp = tempfile.mkdtemp() 574 | os.chdir(temp) 575 | clover_lzma = c["name"] 576 | self.dl.stream_to_file(c["url"], os.path.join(temp, c["name"])) 577 | print("") # Empty space to clear the download progress 578 | if not os.path.exists(os.path.join(temp, c["name"])): 579 | shutil.rmtree(temp,ignore_errors=True) 580 | print(" - Download failed. Aborting...") 581 | print("") 582 | self.u.grab("Press [enter] to return...") 583 | return 584 | # Got a valid file in our temp dir 585 | print("Extracting {}...".format(clover_lzma)) 586 | out = self.r.run({"args":[self.z_path, "e", os.path.join(temp,clover_lzma)]}) 587 | if out[2] != 0: 588 | shutil.rmtree(temp,ignore_errors=True) 589 | print(" - An error occurred extracting: {}".format(out[2])) 590 | print("") 591 | self.u.grab("Press [enter] to return...") 592 | return 593 | # Should result in a .tar file 594 | clover_tar = next((x for x in os.listdir(temp) if x.lower().endswith(".tar")),None) 595 | if not clover_tar: 596 | shutil.rmtree(temp,ignore_errors=True) 597 | print(" - No .tar archive found - aborting...") 598 | print("") 599 | self.u.grab("Press [enter] to return...") 600 | return 601 | # Got the .tar archive - get the .iso 602 | print("Extracting {}...".format(clover_tar)) 603 | out = self.r.run({"args":[self.z_path, "e", os.path.join(temp,clover_tar)]}) 604 | if out[2] != 0: 605 | shutil.rmtree(temp,ignore_errors=True) 606 | print(" - An error occurred extracting: {}".format(out[2])) 607 | print("") 608 | self.u.grab("Press [enter] to return...") 609 | return 610 | # Should result in a .iso file 611 | clover_iso = next((x for x in os.listdir(temp) if x.lower().endswith(".iso")),None) 612 | if not clover_tar: 613 | shutil.rmtree(temp,ignore_errors=True) 614 | print(" - No .iso found - aborting...") 615 | print("") 616 | self.u.grab("Press [enter] to return...") 617 | return 618 | # Got the .iso - let's extract the needed parts 619 | print("Extracting EFI from {}...".format(clover_iso)) 620 | out = self.r.run({"args":[self.z_path, "x", os.path.join(temp,clover_iso), "EFI*"]}) 621 | if out[2] != 0: 622 | shutil.rmtree(temp,ignore_errors=True) 623 | print(" - An error occurred extracting: {}".format(out[2])) 624 | print("") 625 | self.u.grab("Press [enter] to return...") 626 | return 627 | print("Extracting {} from {}...".format(self.boot0,clover_iso)) 628 | out = self.r.run({"args":[self.z_path, "e", os.path.join(temp,clover_iso), self.boot0, "-r"]}) 629 | if out[2] != 0: 630 | shutil.rmtree(temp,ignore_errors=True) 631 | print(" - An error occurred extracting: {}".format(out[2])) 632 | print("") 633 | self.u.grab("Press [enter] to return...") 634 | return 635 | print("Extracting {} from {}...".format(self.boot1,clover_iso)) 636 | out = self.r.run({"args":[self.z_path, "e", os.path.join(temp,clover_iso), self.boot1, "-r"]}) 637 | if out[2] != 0: 638 | shutil.rmtree(temp,ignore_errors=True) 639 | print(" - An error occurred extracting: {}".format(out[2])) 640 | print("") 641 | self.u.grab("Press [enter] to return...") 642 | return 643 | print("Extracting {} from {}...".format(self.boot,clover_iso)) 644 | out = self.r.run({"args":[self.z_path, "e", os.path.join(temp,clover_iso), self.boot, "-r"]}) 645 | if out[2] != 0: 646 | shutil.rmtree(temp,ignore_errors=True) 647 | print(" - An error occurred extracting: {}".format(out[2])) 648 | print("") 649 | self.u.grab("Press [enter] to return...") 650 | return 651 | # At this point, we should have a boot0xx file and an EFI folder in the temp dir 652 | # We need to udpate the disk list though - to reflect the current file system on part 1 653 | # of our current disk 654 | self.d.update() # assumes our disk number stays the same 655 | # Some users are having issues with the "partitions" key not populating - possibly a 3rd party disk management soft? 656 | # Possibly a bad USB? 657 | # We'll see if the key exists - if not, we'll throw an error. 658 | if self.d.disks[str(disk["index"])].get("partitions",None) == None: 659 | # No partitions found. 660 | shutil.rmtree(temp,ignore_errors=True) 661 | print("No partitions located on disk!") 662 | print("") 663 | self.u.grab("Press [enter] to return...") 664 | return 665 | part = self.d.disks[str(disk["index"])]["partitions"].get("0",{}).get("letter",None) # get the first partition's letter 666 | if part == None: 667 | shutil.rmtree(temp,ignore_errors=True) 668 | print("Lost original disk - or formatting failed!") 669 | print("") 670 | self.u.grab("Press [enter] to return...") 671 | return 672 | # Here we have our disk and partitions and such - the CLOVER partition 673 | # will be the first partition 674 | # Let's copy over the EFI folder and then dd the boot0xx file 675 | print("Copying EFI folder to {}/EFI...".format(part)) 676 | if os.path.exists("{}/EFI".format(part)): 677 | print(" - EFI exists - removing...") 678 | shutil.rmtree("{}/EFI".format(part),ignore_errors=True) 679 | time.sleep(1) # Added because windows is dumb 680 | shutil.copytree(os.path.join(temp,"EFI"), "{}/EFI".format(part)) 681 | # Copy boot6 over to the root of the EFI volume - and rename it to boot 682 | print("Copying {} to {}/boot...".format(self.boot,part)) 683 | shutil.copy(os.path.join(temp,self.boot),"{}/boot".format(part)) 684 | # Use bootice to update the MBR and PBR - always on the first 685 | # partition (which is 0 in bootice) 686 | print("Updating the MBR with {}...".format(self.boot0)) 687 | args = [ 688 | os.path.join(self.s_path,self.bi_name), 689 | "/device={}".format(disk.get("index",-1)), 690 | "/mbr", 691 | "/restore", 692 | "/file={}".format(os.path.join(temp,self.boot0)), 693 | "/keep_dpt", 694 | "/quiet" 695 | ] 696 | out = self.r.run({"args":args}) 697 | if out[2] != 0: 698 | shutil.rmtree(temp,ignore_errors=True) 699 | print(" - An error occurred updating the MBR: {}".format(out[2])) 700 | print("") 701 | self.u.grab("Press [enter] to return...") 702 | return 703 | print("Updating the PBR with {}...".format(self.boot1)) 704 | args = [ 705 | os.path.join(self.s_path,self.bi_name), 706 | "/device={}:0".format(disk.get("index",-1)), 707 | "/pbr", 708 | "/restore", 709 | "/file={}".format(os.path.join(temp,self.boot1)), 710 | "/keep_bpb", 711 | "/quiet" 712 | ] 713 | out = self.r.run({"args":args}) 714 | if out[2] != 0: 715 | shutil.rmtree(temp,ignore_errors=True) 716 | print(" - An error occurred updating the PBR: {}".format(out[2])) 717 | print("") 718 | self.u.grab("Press [enter] to return...") 719 | return 720 | print("Cleaning up...") 721 | shutil.rmtree(temp,ignore_errors=True) 722 | print("") 723 | print("Done.") 724 | print("") 725 | self.u.grab("Press [enter] to return to the main menu...") 726 | 727 | def main(self): 728 | # Start out with our cd in the right spot. 729 | os.chdir(os.path.dirname(os.path.realpath(__file__))) 730 | # Let's make sure we have the required files needed 731 | self.u.head("Checking Required Tools") 732 | print("") 733 | if not self.check_dd(): 734 | print("Couldn't find or install {} - aborting!\n".format(self.dd_name)) 735 | self.u.grab("Press [enter] to exit...") 736 | exit(1) 737 | if not self.check_7z(): 738 | print("Couldn't find or install {} - aborting!\n".format(self.z_name)) 739 | self.u.grab("Press [enter] to exit...") 740 | exit(1) 741 | if not self.check_bi(): 742 | print("Couldn't find or install {} - aborting!\n".format(self.bi_name)) 743 | self.u.grab("Press [enter] to exit...") 744 | exit(1) 745 | # Let's just setup a real simple interface and try to write some data 746 | self.u.head("Gathering Disk Info") 747 | print("") 748 | print("Populating list...") 749 | self.d.update() 750 | print("") 751 | print("Done!") 752 | # Let's serve up a list of *only* removable media 753 | self.u.head("Potential Removable Media") 754 | print("") 755 | rem_disks = self.get_disks_of_type(self.d.disks) if not self.show_all_disks else self.d.disks 756 | 757 | # Types: 0 = Unknown, 1 = No Root Dir, 2 = Removable, 3 = Local, 4 = Network, 5 = Disc, 6 = RAM disk 758 | 759 | if self.show_all_disks: 760 | print("!WARNING! This list includes ALL disk types.") 761 | print("!WARNING! Be ABSOLUTELY sure before selecting") 762 | print("!WARNING! a disk!") 763 | else: 764 | print("!WARNING! This list includes both Removable AND") 765 | print("!WARNING! Unknown disk types. Be ABSOLUTELY sure") 766 | print("!WARNING! before selecting a disk!") 767 | print("") 768 | for disk in sorted(rem_disks,key=lambda x:int(x)): 769 | print("{}. {} - {} ({})".format( 770 | disk, 771 | rem_disks[disk].get("model","Unknown"), 772 | self.dl.get_size(rem_disks[disk].get("size",-1),strip_zeroes=True), 773 | ["Unknown","No Root Dir","Removable","Local","Network","Disc","RAM Disk"][rem_disks[disk].get("type",0)] 774 | )) 775 | if not len(rem_disks[disk].get("partitions",{})): 776 | print(" No Mounted Partitions") 777 | else: 778 | parts = rem_disks[disk]["partitions"] 779 | for p in sorted(parts,key=lambda x:int(x)): 780 | print(" {}. {} ({}) {} - {}".format( 781 | p, 782 | parts[p].get("letter","No Letter"), 783 | "No Name" if not parts[p].get("name",None) else parts[p].get("name","No Name"), 784 | parts[p].get("file system","Unknown FS"), 785 | self.dl.get_size(parts[p].get("size",-1),strip_zeroes=True) 786 | )) 787 | print("") 788 | print("Q. Quit") 789 | print("") 790 | print("Usage: [drive number][option (only one allowed)] r[Clover revision (optional)]\n (eg. 1B r5092)") 791 | print(" Options are as follows with precedence B > E > U > G:") 792 | print(" B = Only install the boot manager to the drive's first partition.") 793 | print(" O = Use OpenCore instead of Clover.") 794 | print(" E = Sets the type of the drive's first partition to EFI.") 795 | print(" U = Similar to E, but sets the type to Basic Data (useful for editing).") 796 | print(" G = Format as GPT (default is MBR).") 797 | print(" D = Used without a drive number, toggles showing all disks (currently {}).".format("ENABLED" if self.show_all_disks else "DISABLED")) 798 | print("") 799 | menu = self.u.grab("Please select a disk or press [enter] with no options to refresh: ") 800 | if not len(menu): 801 | self.main() 802 | return 803 | if menu.lower() == "q": 804 | self.u.custom_quit() 805 | if menu.lower() == "d": 806 | self.show_all_disks ^= True 807 | self.main() 808 | return 809 | only_boot = use_oc = set_efi = unset_efi = use_gpt = False 810 | if "b" in menu.lower(): 811 | only_boot = True 812 | menu = menu.lower().replace("b","") 813 | if "o" in menu.lower(): 814 | use_oc = True 815 | menu = menu.lower().replace("o","") 816 | if "e" in menu.lower(): 817 | set_efi = True 818 | menu = menu.lower().replace("e","") 819 | if "u" in menu.lower(): 820 | unset_efi = True 821 | menu = menu.lower().replace("u","") 822 | if "g" in menu.lower(): 823 | use_gpt = True 824 | menu = menu.lower().replace("g","") 825 | 826 | # Extract Clover version from args if found 827 | clover_list = [x for x in menu.split() if x.lower().startswith("r") and all(y in "0123456789" for y in x[1:])] 828 | menu = " ".join([x for x in menu.split() if not x in clover_list]) 829 | clover_version = None if not len(clover_list) else clover_list[0][1:] # Skip the "r" prefix 830 | 831 | # Prepare for OC if need be 832 | if use_oc: clover_version = "OpenCore" 833 | 834 | selected_disk = rem_disks.get(menu.strip(),None) 835 | if not selected_disk: 836 | self.u.head("Invalid Choice") 837 | print("") 838 | print("Disk {} is not an option.".format(menu)) 839 | print("") 840 | self.u.grab("Returning in 5 seconds...", timeout=5) 841 | self.main() 842 | return 843 | # Got a disk! 844 | if only_boot: 845 | if use_oc: self.install_oc(selected_disk) 846 | else: self.install_clover(selected_disk, clover_version) 847 | elif set_efi: 848 | self.diskpart_flag(selected_disk, True) 849 | elif unset_efi: 850 | self.diskpart_flag(selected_disk, False) 851 | else: 852 | # Check erase 853 | while True: 854 | self.u.head("Erase {}".format(selected_disk.get("model","Unknown"))) 855 | print("") 856 | print("{}. {} - {} ({})".format( 857 | selected_disk.get("index",-1), 858 | selected_disk.get("model","Unknown"), 859 | self.dl.get_size(selected_disk.get("size",-1),strip_zeroes=True), 860 | ["Unknown","No Root Dir","Removable","Local","Network","Disc","RAM Disk"][selected_disk.get("type",0)] 861 | )) 862 | print("") 863 | print("If you continue - THIS DISK WILL BE ERASED") 864 | print("ALL DATA WILL BE LOST AND ALL PARTITIONS WILL") 865 | print("BE REMOVED!!!!!!!") 866 | print("") 867 | yn = self.u.grab("Continue? (y/n): ") 868 | if yn.lower() == "n": 869 | self.main() 870 | return 871 | if yn.lower() == "y": 872 | break 873 | # Got the OK to erase! Let's format a diskpart script! 874 | self.diskpart_erase(selected_disk, use_gpt, clover_version) 875 | self.main() 876 | 877 | if __name__ == '__main__': 878 | w = WinUSB() 879 | w.main() 880 | -------------------------------------------------------------------------------- /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 | Thanks to: 6 | 7 | * 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 8 | * munki for his [macadmin-scripts](https://github.com/munki/macadmin-scripts) 9 | * timsutton for [brigadier](https://github.com/timsutton/brigadier) 10 | * 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 11 | -------------------------------------------------------------------------------- /Scripts/BOOTICEx64.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeonme/gibMacOS/a7b54954173ca3ff01946e80f80b1fd02ef0be29/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 = " 3: 32 | # Not enough info there - csv is like: 33 | # 1. Empty row 34 | # 2. Headers 35 | # 3->X-1. Rest of the info 36 | # X. Last empty row 37 | return {} 38 | # New format is: 39 | # Node, Device, Index, Model, Partitions, Size 40 | disks = disks[2:-1] 41 | p_disks = {} 42 | for d in disks: 43 | # Skip the Node value 44 | ds = d[1:] 45 | if len(ds) < 5: 46 | continue 47 | p_disks[ds[1]] = { 48 | "device":ds[0], 49 | "model":" ".join(ds[2:-2]), 50 | "type":0 # 0 = Unknown, 1 = No Root Dir, 2 = Removable, 3 = Local, 4 = Network, 5 = Disc, 6 = RAM disk 51 | } 52 | # More fault-tolerance with ints 53 | p_disks[ds[1]]["index"] = int(ds[1]) if len(ds[1]) else -1 54 | p_disks[ds[1]]["size"] = int(ds[-1]) if len(ds[-1]) else -1 55 | p_disks[ds[1]]["partitioncount"] = int(ds[-2]) if len(ds[-2]) else 0 56 | 57 | if not len(p_disks): 58 | # Drat, nothing 59 | return p_disks 60 | # Let's find a shitty way to map this biz now 61 | shit = self.r.run({"args":[self.wmic, "path", "Win32_LogicalDiskToPartition", "get", "antecedent,dependent"]})[0] 62 | shit = shit.replace("\r","").split("\n")[1:] 63 | for s in shit: 64 | s = s.lower() 65 | d = p = mp = None 66 | try: 67 | dp = s.split("deviceid=")[1].split('"')[1] 68 | d = dp.split("disk #")[1].split(",")[0] 69 | p = dp.split("partition #")[1] 70 | mp = s.split("deviceid=")[2].split('"')[1].upper() 71 | except: 72 | pass 73 | if any([d, p, mp]): 74 | # Got *something* 75 | if p_disks.get(d,None): 76 | if not p_disks[d].get("partitions",None): 77 | p_disks[d]["partitions"] = {} 78 | p_disks[d]["partitions"][p] = {"letter":mp} 79 | # Last attempt to do this - let's get the partition names! 80 | parts = self.r.run({"args":[self.wmic, "logicaldisk", "get", "deviceid,filesystem,volumename,size,drivetype", "/format:csv"]})[0] 81 | cspart = csv.reader(parts.replace("\r","").split("\n"), delimiter=",") 82 | parts = list(cspart) 83 | if not len(parts) > 2: 84 | return p_disks 85 | parts = parts[2:-1] 86 | for p in parts: 87 | # Again, skip the Node value 88 | ps = p[1:] 89 | if len(ps) < 2: 90 | # Need the drive letter and disk type at minimum 91 | continue 92 | # Organize! 93 | plt = ps[0] # get letter 94 | ptp = ps[1] # get disk type 95 | # Initialize 96 | pfs = pnm = None 97 | psz = -1 # Set to -1 initially for indeterminate size 98 | try: 99 | pfs = ps[2] # get file system 100 | psz = ps[3] # get size 101 | pnm = ps[4] # get the rest in the name 102 | except: 103 | pass 104 | for d in p_disks: 105 | p_dict = p_disks[d] 106 | for pr in p_dict.get("partitions",{}): 107 | pr = p_dict["partitions"][pr] 108 | if pr.get("letter","").upper() == plt.upper(): 109 | # Found it - set all attributes 110 | pr["size"] = int(psz) if len(psz) else -1 111 | pr["file system"] = pfs 112 | pr["name"] = pnm 113 | # Also need to set the parent drive's type 114 | if len(ptp): 115 | p_dict["type"] = int(ptp) 116 | break 117 | return p_disks 118 | -------------------------------------------------------------------------------- /Scripts/downloader.py: -------------------------------------------------------------------------------- 1 | import sys, os, time, ssl, gzip 2 | from io import BytesIO 3 | # Python-aware urllib stuff 4 | if sys.version_info >= (3, 0): 5 | from urllib.request import urlopen, Request 6 | else: 7 | # Import urllib2 to catch errors 8 | import urllib2 9 | from urllib2 import urlopen, Request 10 | 11 | class Downloader: 12 | 13 | def __init__(self,**kwargs): 14 | self.ua = kwargs.get("useragent",{"User-Agent":"Mozilla"}) 15 | self.chunk = 1048576 # 1024 x 1024 i.e. 1MiB 16 | 17 | # Provide reasonable default logic to workaround macOS CA file handling 18 | cafile = ssl.get_default_verify_paths().openssl_cafile 19 | try: 20 | # If default OpenSSL CA file does not exist, use that from certifi 21 | if not os.path.exists(cafile): 22 | import certifi 23 | cafile = certifi.where() 24 | self.ssl_context = ssl.create_default_context(cafile=cafile) 25 | except: 26 | # None of the above worked, disable certificate verification for now 27 | self.ssl_context = ssl._create_unverified_context() 28 | return 29 | 30 | def _decode(self, value, encoding="utf-8", errors="ignore"): 31 | # Helper method to only decode if bytes type 32 | if sys.version_info >= (3,0) and isinstance(value, bytes): 33 | return value.decode(encoding,errors) 34 | return value 35 | 36 | def open_url(self, url, headers = None): 37 | # Fall back on the default ua if none provided 38 | headers = self.ua if headers == None else headers 39 | # Wrap up the try/except block so we don't have to do this for each function 40 | try: 41 | response = urlopen(Request(url, headers=headers), context=self.ssl_context) 42 | except Exception as e: 43 | # No fixing this - bail 44 | return None 45 | return response 46 | 47 | def get_size(self, size, suffix=None, use_1024=False, round_to=2, strip_zeroes=False): 48 | # size is the number of bytes 49 | # suffix is the target suffix to locate (B, KB, MB, etc) - if found 50 | # use_2014 denotes whether or not we display in MiB vs MB 51 | # round_to is the number of dedimal points to round our result to (0-15) 52 | # strip_zeroes denotes whether we strip out zeroes 53 | 54 | # Failsafe in case our size is unknown 55 | if size == -1: 56 | return "Unknown" 57 | # Get our suffixes based on use_1024 58 | ext = ["B","KiB","MiB","GiB","TiB","PiB"] if use_1024 else ["B","KB","MB","GB","TB","PB"] 59 | div = 1024 if use_1024 else 1000 60 | s = float(size) 61 | s_dict = {} # Initialize our dict 62 | # Iterate the ext list, and divide by 1000 or 1024 each time to setup the dict {ext:val} 63 | for e in ext: 64 | s_dict[e] = s 65 | s /= div 66 | # Get our suffix if provided - will be set to None if not found, or if started as None 67 | suffix = next((x for x in ext if x.lower() == suffix.lower()),None) if suffix else suffix 68 | # Get the largest value that's still over 1 69 | biggest = suffix if suffix else next((x for x in ext[::-1] if s_dict[x] >= 1), "B") 70 | # Determine our rounding approach - first make sure it's an int; default to 2 on error 71 | try:round_to=int(round_to) 72 | except:round_to=2 73 | round_to = 0 if round_to < 0 else 15 if round_to > 15 else round_to # Ensure it's between 0 and 15 74 | bval = round(s_dict[biggest], round_to) 75 | # Split our number based on decimal points 76 | a,b = str(bval).split(".") 77 | # Check if we need to strip or pad zeroes 78 | b = b.rstrip("0") if strip_zeroes else b.ljust(round_to,"0") if round_to > 0 else "" 79 | return "{:,}{} {}".format(int(a),"" if not b else "."+b,biggest) 80 | 81 | def _progress_hook(self, response, bytes_so_far, total_size): 82 | if total_size > 0: 83 | percent = float(bytes_so_far) / total_size 84 | percent = round(percent*100, 2) 85 | t_s = self.get_size(total_size) 86 | try: b_s = self.get_size(bytes_so_far, t_s.split(" ")[1]) 87 | except: b_s = self.get_size(bytes_so_far) 88 | sys.stdout.write("Downloaded {} of {} ({:.2f}%)\r".format(b_s, t_s, percent)) 89 | else: 90 | b_s = self.get_size(bytes_so_far) 91 | sys.stdout.write("Downloaded {}\r".format(b_s)) 92 | 93 | def get_string(self, url, progress = True, headers = None, expand_gzip = True): 94 | response = self.get_bytes(url,progress,headers,expand_gzip) 95 | if response == None: return None 96 | return self._decode(response) 97 | 98 | def get_bytes(self, url, progress = True, headers = None, expand_gzip = True): 99 | response = self.open_url(url, headers) 100 | if response == None: return None 101 | bytes_so_far = 0 102 | try: total_size = int(response.headers['Content-Length']) 103 | except: total_size = -1 104 | chunk_so_far = b"" 105 | while True: 106 | chunk = response.read(self.chunk) 107 | bytes_so_far += len(chunk) 108 | if progress: self._progress_hook(response, bytes_so_far, total_size) 109 | if not chunk: break 110 | chunk_so_far += chunk 111 | if expand_gzip and response.headers.get("Content-Encoding","unknown").lower() == "gzip": 112 | fileobj = BytesIO(chunk_so_far) 113 | gfile = gzip.GzipFile(fileobj=fileobj) 114 | return gfile.read() 115 | return chunk_so_far 116 | 117 | def stream_to_file(self, url, file_path, progress = True, headers = None): 118 | response = self.open_url(url, headers) 119 | if response == None: return None 120 | bytes_so_far = 0 121 | try: total_size = int(response.headers['Content-Length']) 122 | except: total_size = -1 123 | with open(file_path, 'wb') as f: 124 | while True: 125 | chunk = response.read(self.chunk) 126 | bytes_so_far += len(chunk) 127 | if progress: self._progress_hook(response, bytes_so_far, total_size) 128 | if not chunk: break 129 | f.write(chunk) 130 | return file_path if os.path.exists(file_path) else None 131 | -------------------------------------------------------------------------------- /Scripts/plist.py: -------------------------------------------------------------------------------- 1 | ### ### 2 | # Imports # 3 | ### ### 4 | 5 | import datetime, os, plistlib, struct, sys, itertools 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 | 13 | try: 14 | basestring # Python 2 15 | unicode 16 | except NameError: 17 | basestring = str # Python 3 18 | unicode = str 19 | 20 | try: 21 | FMT_XML = plistlib.FMT_XML 22 | FMT_BINARY = plistlib.FMT_BINARY 23 | except AttributeError: 24 | FMT_XML = "FMT_XML" 25 | FMT_BINARY = "FMT_BINARY" 26 | 27 | ### ### 28 | # Helper Methods # 29 | ### ### 30 | 31 | def wrap_data(value): 32 | if not _check_py3(): return plistlib.Data(value) 33 | return value 34 | 35 | def extract_data(value): 36 | if not _check_py3() and isinstance(value,plistlib.Data): return value.data 37 | return value 38 | 39 | def _check_py3(): 40 | return sys.version_info >= (3, 0) 41 | 42 | def _is_binary(fp): 43 | if isinstance(fp, basestring): 44 | return fp.startswith(b"bplist00") 45 | header = fp.read(32) 46 | fp.seek(0) 47 | return header[:8] == b'bplist00' 48 | 49 | ### ### 50 | # Deprecated Functions - Remapped # 51 | ### ### 52 | 53 | def readPlist(pathOrFile): 54 | if not isinstance(pathOrFile, basestring): 55 | return load(pathOrFile) 56 | with open(pathOrFile, "rb") as f: 57 | return load(f) 58 | 59 | def writePlist(value, pathOrFile): 60 | if not isinstance(pathOrFile, basestring): 61 | return dump(value, pathOrFile, fmt=FMT_XML, sort_keys=True, skipkeys=False) 62 | with open(pathOrFile, "wb") as f: 63 | return dump(value, f, fmt=FMT_XML, sort_keys=True, skipkeys=False) 64 | 65 | ### ### 66 | # Remapped Functions # 67 | ### ### 68 | 69 | def load(fp, fmt=None, use_builtin_types=None, dict_type=dict): 70 | if _check_py3(): 71 | use_builtin_types = True if use_builtin_types == None else use_builtin_types 72 | # We need to monkey patch this to allow for hex integers - code taken/modified from 73 | # https://github.com/python/cpython/blob/3.8/Lib/plistlib.py 74 | if fmt is None: 75 | header = fp.read(32) 76 | fp.seek(0) 77 | for info in plistlib._FORMATS.values(): 78 | if info['detect'](header): 79 | P = info['parser'] 80 | break 81 | else: 82 | raise plistlib.InvalidFileException() 83 | else: 84 | P = plistlib._FORMATS[fmt]['parser'] 85 | try: 86 | p = P(use_builtin_types=use_builtin_types, dict_type=dict_type) 87 | except: 88 | # Python 3.9 removed use_builtin_types 89 | p = P(dict_type=dict_type) 90 | if isinstance(p,plistlib._PlistParser): 91 | # Monkey patch! 92 | def end_integer(): 93 | d = p.get_data() 94 | p.add_object(int(d,16) if d.lower().startswith("0x") else int(d)) 95 | p.end_integer = end_integer 96 | return p.parse(fp) 97 | elif not _is_binary(fp): 98 | # Is not binary - assume a string - and try to load 99 | # We avoid using readPlistFromString() as that uses 100 | # cStringIO and fails when Unicode strings are detected 101 | # Don't subclass - keep the parser local 102 | from xml.parsers.expat import ParserCreate 103 | # Create a new PlistParser object - then we need to set up 104 | # the values and parse. 105 | p = plistlib.PlistParser() 106 | # We also need to monkey patch this to allow for other dict_types 107 | def begin_dict(attrs): 108 | d = dict_type() 109 | p.addObject(d) 110 | p.stack.append(d) 111 | def end_integer(): 112 | d = p.getData() 113 | p.addObject(int(d,16) if d.lower().startswith("0x") else int(d)) 114 | p.begin_dict = begin_dict 115 | p.end_integer = end_integer 116 | parser = ParserCreate() 117 | parser.StartElementHandler = p.handleBeginElement 118 | parser.EndElementHandler = p.handleEndElement 119 | parser.CharacterDataHandler = p.handleData 120 | if isinstance(fp, unicode): 121 | # Encode unicode -> string; use utf-8 for safety 122 | fp = fp.encode("utf-8") 123 | if isinstance(fp, basestring): 124 | # It's a string - let's wrap it up 125 | fp = StringIO(fp) 126 | # Parse it 127 | parser.ParseFile(fp) 128 | return p.root 129 | else: 130 | use_builtin_types = False if use_builtin_types == None else use_builtin_types 131 | try: 132 | p = _BinaryPlistParser(use_builtin_types=use_builtin_types, dict_type=dict_type) 133 | except: 134 | # Python 3.9 removed use_builtin_types 135 | p = _BinaryPlistParser(dict_type=dict_type) 136 | return p.parse(fp) 137 | 138 | def loads(value, fmt=None, use_builtin_types=None, dict_type=dict): 139 | if _check_py3() and isinstance(value, basestring): 140 | # If it's a string - encode it 141 | value = value.encode() 142 | try: 143 | return load(BytesIO(value),fmt=fmt,use_builtin_types=use_builtin_types,dict_type=dict_type) 144 | except: 145 | # Python 3.9 removed use_builtin_types 146 | return load(BytesIO(value),fmt=fmt,dict_type=dict_type) 147 | def dump(value, fp, fmt=FMT_XML, sort_keys=True, skipkeys=False): 148 | if _check_py3(): 149 | plistlib.dump(value, fp, fmt=fmt, sort_keys=sort_keys, skipkeys=skipkeys) 150 | else: 151 | if fmt == FMT_XML: 152 | # We need to monkey patch a bunch here too in order to avoid auto-sorting 153 | # of keys 154 | writer = plistlib.PlistWriter(fp) 155 | def writeDict(d): 156 | if d: 157 | writer.beginElement("dict") 158 | items = sorted(d.items()) if sort_keys else d.items() 159 | for key, value in items: 160 | if not isinstance(key, basestring): 161 | if skipkeys: 162 | continue 163 | raise TypeError("keys must be strings") 164 | writer.simpleElement("key", key) 165 | writer.writeValue(value) 166 | writer.endElement("dict") 167 | else: 168 | writer.simpleElement("dict") 169 | writer.writeDict = writeDict 170 | writer.writeln("") 171 | writer.writeValue(value) 172 | writer.writeln("") 173 | elif fmt == FMT_BINARY: 174 | # Assume binary at this point 175 | writer = _BinaryPlistWriter(fp, sort_keys=sort_keys, skipkeys=skipkeys) 176 | writer.write(value) 177 | else: 178 | # Not a proper format 179 | raise ValueError("Unsupported format: {}".format(fmt)) 180 | 181 | def dumps(value, fmt=FMT_XML, skipkeys=False, sort_keys=True): 182 | if _check_py3(): 183 | return plistlib.dumps(value, fmt=fmt, skipkeys=skipkeys, sort_keys=sort_keys).decode("utf-8") 184 | else: 185 | # We avoid using writePlistToString() as that uses 186 | # cStringIO and fails when Unicode strings are detected 187 | f = StringIO() 188 | dump(value, f, fmt=fmt, skipkeys=skipkeys, sort_keys=sort_keys) 189 | return f.getvalue() 190 | 191 | ### ### 192 | # Binary Plist Stuff For Py2 # 193 | ### ### 194 | 195 | # From the python 3 plistlib.py source: https://github.com/python/cpython/blob/3.7/Lib/plistlib.py 196 | # Tweaked to function on Python 2 197 | 198 | class InvalidFileException (ValueError): 199 | def __init__(self, message="Invalid file"): 200 | ValueError.__init__(self, message) 201 | 202 | _BINARY_FORMAT = {1: 'B', 2: 'H', 4: 'L', 8: 'Q'} 203 | 204 | _undefined = object() 205 | 206 | class _BinaryPlistParser: 207 | """ 208 | Read or write a binary plist file, following the description of the binary 209 | format. Raise InvalidFileException in case of error, otherwise return the 210 | root object. 211 | see also: http://opensource.apple.com/source/CF/CF-744.18/CFBinaryPList.c 212 | """ 213 | def __init__(self, use_builtin_types, dict_type): 214 | self._use_builtin_types = use_builtin_types 215 | self._dict_type = dict_type 216 | 217 | def parse(self, fp): 218 | try: 219 | # The basic file format: 220 | # HEADER 221 | # object... 222 | # refid->offset... 223 | # TRAILER 224 | self._fp = fp 225 | self._fp.seek(-32, os.SEEK_END) 226 | trailer = self._fp.read(32) 227 | if len(trailer) != 32: 228 | raise InvalidFileException() 229 | ( 230 | offset_size, self._ref_size, num_objects, top_object, 231 | offset_table_offset 232 | ) = struct.unpack('>6xBBQQQ', trailer) 233 | self._fp.seek(offset_table_offset) 234 | self._object_offsets = self._read_ints(num_objects, offset_size) 235 | self._objects = [_undefined] * num_objects 236 | return self._read_object(top_object) 237 | 238 | except (OSError, IndexError, struct.error, OverflowError, 239 | UnicodeDecodeError): 240 | raise InvalidFileException() 241 | 242 | def _get_size(self, tokenL): 243 | """ return the size of the next object.""" 244 | if tokenL == 0xF: 245 | m = ord(self._fp.read(1)[0]) & 0x3 246 | s = 1 << m 247 | f = '>' + _BINARY_FORMAT[s] 248 | return struct.unpack(f, self._fp.read(s))[0] 249 | 250 | return tokenL 251 | 252 | def _read_ints(self, n, size): 253 | data = self._fp.read(size * n) 254 | if size in _BINARY_FORMAT: 255 | return struct.unpack('>' + _BINARY_FORMAT[size] * n, data) 256 | else: 257 | if not size or len(data) != size * n: 258 | raise InvalidFileException() 259 | return tuple(int.from_bytes(data[i: i + size], 'big') 260 | for i in range(0, size * n, size)) 261 | 262 | def _read_refs(self, n): 263 | return self._read_ints(n, self._ref_size) 264 | 265 | def _read_object(self, ref): 266 | """ 267 | read the object by reference. 268 | May recursively read sub-objects (content of an array/dict/set) 269 | """ 270 | result = self._objects[ref] 271 | if result is not _undefined: 272 | return result 273 | 274 | offset = self._object_offsets[ref] 275 | self._fp.seek(offset) 276 | token = ord(self._fp.read(1)[0]) 277 | tokenH, tokenL = token & 0xF0, token & 0x0F 278 | 279 | if token == 0: # \x00 or 0x00 280 | result = None 281 | 282 | elif token == 8: # \x08 or 0x08 283 | result = False 284 | 285 | elif token == 9: # \x09 or 0x09 286 | result = True 287 | 288 | # The referenced source code also mentions URL (0x0c, 0x0d) and 289 | # UUID (0x0e), but neither can be generated using the Cocoa libraries. 290 | 291 | elif token == 15: # \x0f or 0x0f 292 | result = b'' 293 | 294 | elif tokenH == 0x10: # int 295 | result = 0 296 | for k in range((2 << tokenL) - 1): 297 | result = (result << 8) + ord(self._fp.read(1)) 298 | # result = int.from_bytes(self._fp.read(1 << tokenL), 299 | # 'big', signed=tokenL >= 3) 300 | 301 | elif token == 0x22: # real 302 | result = struct.unpack('>f', self._fp.read(4))[0] 303 | 304 | elif token == 0x23: # real 305 | result = struct.unpack('>d', self._fp.read(8))[0] 306 | 307 | elif token == 0x33: # date 308 | f = struct.unpack('>d', self._fp.read(8))[0] 309 | # timestamp 0 of binary plists corresponds to 1/1/2001 310 | # (year of Mac OS X 10.0), instead of 1/1/1970. 311 | result = (datetime.datetime(2001, 1, 1) + 312 | datetime.timedelta(seconds=f)) 313 | 314 | elif tokenH == 0x40: # data 315 | s = self._get_size(tokenL) 316 | if self._use_builtin_types: 317 | result = self._fp.read(s) 318 | else: 319 | result = plistlib.Data(self._fp.read(s)) 320 | 321 | elif tokenH == 0x50: # ascii string 322 | s = self._get_size(tokenL) 323 | result = self._fp.read(s).decode('ascii') 324 | result = result 325 | 326 | elif tokenH == 0x60: # unicode string 327 | s = self._get_size(tokenL) 328 | result = self._fp.read(s * 2).decode('utf-16be') 329 | 330 | # tokenH == 0x80 is documented as 'UID' and appears to be used for 331 | # keyed-archiving, not in plists. 332 | 333 | elif tokenH == 0xA0: # array 334 | s = self._get_size(tokenL) 335 | obj_refs = self._read_refs(s) 336 | result = [] 337 | self._objects[ref] = result 338 | result.extend(self._read_object(x) for x in obj_refs) 339 | 340 | # tokenH == 0xB0 is documented as 'ordset', but is not actually 341 | # implemented in the Apple reference code. 342 | 343 | # tokenH == 0xC0 is documented as 'set', but sets cannot be used in 344 | # plists. 345 | 346 | elif tokenH == 0xD0: # dict 347 | s = self._get_size(tokenL) 348 | key_refs = self._read_refs(s) 349 | obj_refs = self._read_refs(s) 350 | result = self._dict_type() 351 | self._objects[ref] = result 352 | for k, o in zip(key_refs, obj_refs): 353 | key = self._read_object(k) 354 | if isinstance(key, plistlib.Data): 355 | key = key.data 356 | result[key] = self._read_object(o) 357 | 358 | else: 359 | raise InvalidFileException() 360 | 361 | self._objects[ref] = result 362 | return result 363 | 364 | def _count_to_size(count): 365 | if count < 1 << 8: 366 | return 1 367 | 368 | elif count < 1 << 16: 369 | return 2 370 | 371 | elif count << 1 << 32: 372 | return 4 373 | 374 | else: 375 | return 8 376 | 377 | _scalars = (str, int, float, datetime.datetime, bytes) 378 | 379 | class _BinaryPlistWriter (object): 380 | def __init__(self, fp, sort_keys, skipkeys): 381 | self._fp = fp 382 | self._sort_keys = sort_keys 383 | self._skipkeys = skipkeys 384 | 385 | def write(self, value): 386 | 387 | # Flattened object list: 388 | self._objlist = [] 389 | 390 | # Mappings from object->objectid 391 | # First dict has (type(object), object) as the key, 392 | # second dict is used when object is not hashable and 393 | # has id(object) as the key. 394 | self._objtable = {} 395 | self._objidtable = {} 396 | 397 | # Create list of all objects in the plist 398 | self._flatten(value) 399 | 400 | # Size of object references in serialized containers 401 | # depends on the number of objects in the plist. 402 | num_objects = len(self._objlist) 403 | self._object_offsets = [0]*num_objects 404 | self._ref_size = _count_to_size(num_objects) 405 | 406 | self._ref_format = _BINARY_FORMAT[self._ref_size] 407 | 408 | # Write file header 409 | self._fp.write(b'bplist00') 410 | 411 | # Write object list 412 | for obj in self._objlist: 413 | self._write_object(obj) 414 | 415 | # Write refnum->object offset table 416 | top_object = self._getrefnum(value) 417 | offset_table_offset = self._fp.tell() 418 | offset_size = _count_to_size(offset_table_offset) 419 | offset_format = '>' + _BINARY_FORMAT[offset_size] * num_objects 420 | self._fp.write(struct.pack(offset_format, *self._object_offsets)) 421 | 422 | # Write trailer 423 | sort_version = 0 424 | trailer = ( 425 | sort_version, offset_size, self._ref_size, num_objects, 426 | top_object, offset_table_offset 427 | ) 428 | self._fp.write(struct.pack('>5xBBBQQQ', *trailer)) 429 | 430 | def _flatten(self, value): 431 | # First check if the object is in the object table, not used for 432 | # containers to ensure that two subcontainers with the same contents 433 | # will be serialized as distinct values. 434 | if isinstance(value, _scalars): 435 | if (type(value), value) in self._objtable: 436 | return 437 | 438 | elif isinstance(value, plistlib.Data): 439 | if (type(value.data), value.data) in self._objtable: 440 | return 441 | 442 | elif id(value) in self._objidtable: 443 | return 444 | 445 | # Add to objectreference map 446 | refnum = len(self._objlist) 447 | self._objlist.append(value) 448 | if isinstance(value, _scalars): 449 | self._objtable[(type(value), value)] = refnum 450 | elif isinstance(value, plistlib.Data): 451 | self._objtable[(type(value.data), value.data)] = refnum 452 | else: 453 | self._objidtable[id(value)] = refnum 454 | 455 | # And finally recurse into containers 456 | if isinstance(value, dict): 457 | keys = [] 458 | values = [] 459 | items = value.items() 460 | if self._sort_keys: 461 | items = sorted(items) 462 | 463 | for k, v in items: 464 | if not isinstance(k, basestring): 465 | if self._skipkeys: 466 | continue 467 | raise TypeError("keys must be strings") 468 | keys.append(k) 469 | values.append(v) 470 | 471 | for o in itertools.chain(keys, values): 472 | self._flatten(o) 473 | 474 | elif isinstance(value, (list, tuple)): 475 | for o in value: 476 | self._flatten(o) 477 | 478 | def _getrefnum(self, value): 479 | if isinstance(value, _scalars): 480 | return self._objtable[(type(value), value)] 481 | elif isinstance(value, plistlib.Data): 482 | return self._objtable[(type(value.data), value.data)] 483 | else: 484 | return self._objidtable[id(value)] 485 | 486 | def _write_size(self, token, size): 487 | if size < 15: 488 | self._fp.write(struct.pack('>B', token | size)) 489 | 490 | elif size < 1 << 8: 491 | self._fp.write(struct.pack('>BBB', token | 0xF, 0x10, size)) 492 | 493 | elif size < 1 << 16: 494 | self._fp.write(struct.pack('>BBH', token | 0xF, 0x11, size)) 495 | 496 | elif size < 1 << 32: 497 | self._fp.write(struct.pack('>BBL', token | 0xF, 0x12, size)) 498 | 499 | else: 500 | self._fp.write(struct.pack('>BBQ', token | 0xF, 0x13, size)) 501 | 502 | def _write_object(self, value): 503 | ref = self._getrefnum(value) 504 | self._object_offsets[ref] = self._fp.tell() 505 | if value is None: 506 | self._fp.write(b'\x00') 507 | 508 | elif value is False: 509 | self._fp.write(b'\x08') 510 | 511 | elif value is True: 512 | self._fp.write(b'\x09') 513 | 514 | elif isinstance(value, int): 515 | if value < 0: 516 | try: 517 | self._fp.write(struct.pack('>Bq', 0x13, value)) 518 | except struct.error: 519 | raise OverflowError(value) # from None 520 | elif value < 1 << 8: 521 | self._fp.write(struct.pack('>BB', 0x10, value)) 522 | elif value < 1 << 16: 523 | self._fp.write(struct.pack('>BH', 0x11, value)) 524 | elif value < 1 << 32: 525 | self._fp.write(struct.pack('>BL', 0x12, value)) 526 | elif value < 1 << 63: 527 | self._fp.write(struct.pack('>BQ', 0x13, value)) 528 | elif value < 1 << 64: 529 | self._fp.write(b'\x14' + value.to_bytes(16, 'big', signed=True)) 530 | else: 531 | raise OverflowError(value) 532 | 533 | elif isinstance(value, float): 534 | self._fp.write(struct.pack('>Bd', 0x23, value)) 535 | 536 | elif isinstance(value, datetime.datetime): 537 | f = (value - datetime.datetime(2001, 1, 1)).total_seconds() 538 | self._fp.write(struct.pack('>Bd', 0x33, f)) 539 | 540 | elif isinstance(value, plistlib.Data): 541 | self._write_size(0x40, len(value.data)) 542 | self._fp.write(value.data) 543 | 544 | elif isinstance(value, basestring): 545 | try: 546 | t = value.encode('ascii') 547 | self._write_size(0x50, len(value)) 548 | except UnicodeEncodeError: 549 | t = value.encode('utf-16be') 550 | self._write_size(0x60, len(t) // 2) 551 | self._fp.write(t) 552 | 553 | elif isinstance(value, (bytes, bytearray)): 554 | self._write_size(0x40, len(value)) 555 | self._fp.write(value) 556 | 557 | elif isinstance(value, (list, tuple)): 558 | refs = [self._getrefnum(o) for o in value] 559 | s = len(refs) 560 | self._write_size(0xA0, s) 561 | self._fp.write(struct.pack('>' + self._ref_format * s, *refs)) 562 | 563 | elif isinstance(value, dict): 564 | keyRefs, valRefs = [], [] 565 | 566 | if self._sort_keys: 567 | rootItems = sorted(value.items()) 568 | else: 569 | rootItems = value.items() 570 | 571 | for k, v in rootItems: 572 | if not isinstance(k, basestring): 573 | if self._skipkeys: 574 | continue 575 | raise TypeError("keys must be strings") 576 | keyRefs.append(self._getrefnum(k)) 577 | valRefs.append(self._getrefnum(v)) 578 | 579 | s = len(keyRefs) 580 | self._write_size(0xD0, s) 581 | self._fp.write(struct.pack('>' + self._ref_format * s, *keyRefs)) 582 | self._fp.write(struct.pack('>' + self._ref_format * s, *valRefs)) 583 | 584 | else: 585 | raise TypeError(value) 586 | -------------------------------------------------------------------------------- /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"): 13 | self.name = name 14 | # Init our colors before we need to print anything 15 | cwd = os.getcwd() 16 | os.chdir(os.path.dirname(os.path.realpath(__file__))) 17 | if os.path.exists("colors.json"): 18 | self.colors_dict = json.load(open("colors.json")) 19 | else: 20 | self.colors_dict = {} 21 | os.chdir(cwd) 22 | 23 | def check_admin(self): 24 | # Returns whether or not we're admin 25 | try: 26 | is_admin = os.getuid() == 0 27 | except AttributeError: 28 | is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0 29 | return is_admin 30 | 31 | def elevate(self, file): 32 | # Runs the passed file as admin 33 | if self.check_admin(): 34 | return 35 | if os.name == "nt": 36 | ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, file, None, 1) 37 | else: 38 | try: 39 | p = subprocess.Popen(["which", "sudo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 40 | c = p.communicate()[0].decode("utf-8", "ignore").replace("\n", "") 41 | os.execv(c, [ sys.executable, 'python'] + sys.argv) 42 | except: 43 | exit(1) 44 | 45 | def compare_versions(self, vers1, vers2, **kwargs): 46 | # Helper method to compare ##.## strings 47 | # 48 | # vers1 < vers2 = True 49 | # vers1 = vers2 = None 50 | # vers1 > vers2 = False 51 | 52 | # Sanitize the pads 53 | pad = str(kwargs.get("pad", "")) 54 | sep = str(kwargs.get("separator", ".")) 55 | 56 | ignore_case = kwargs.get("ignore_case", True) 57 | 58 | # Cast as strings 59 | vers1 = str(vers1) 60 | vers2 = str(vers2) 61 | 62 | if ignore_case: 63 | vers1 = vers1.lower() 64 | vers2 = vers2.lower() 65 | 66 | # Split and pad lists 67 | v1_parts, v2_parts = self.pad_length(vers1.split(sep), vers2.split(sep)) 68 | 69 | # Iterate and compare 70 | for i in range(len(v1_parts)): 71 | # Remove non-numeric 72 | v1 = ''.join(c.lower() for c in v1_parts[i] if c.isalnum()) 73 | v2 = ''.join(c.lower() for c in v2_parts[i] if c.isalnum()) 74 | # Equalize the lengths 75 | v1, v2 = self.pad_length(v1, v2) 76 | # Compare 77 | if str(v1) < str(v2): 78 | return True 79 | elif str(v1) > str(v2): 80 | return False 81 | # Never differed - return None, must be equal 82 | return None 83 | 84 | def pad_length(self, var1, var2, pad = "0"): 85 | # Pads the vars on the left side to make them equal length 86 | pad = "0" if len(str(pad)) < 1 else str(pad)[0] 87 | if not type(var1) == type(var2): 88 | # Type mismatch! Just return what we got 89 | return (var1, var2) 90 | if len(var1) < len(var2): 91 | if type(var1) is list: 92 | var1.extend([str(pad) for x in range(len(var2) - len(var1))]) 93 | else: 94 | var1 = "{}{}".format((pad*(len(var2)-len(var1))), var1) 95 | elif len(var2) < len(var1): 96 | if type(var2) is list: 97 | var2.extend([str(pad) for x in range(len(var1) - len(var2))]) 98 | else: 99 | var2 = "{}{}".format((pad*(len(var1)-len(var2))), var2) 100 | return (var1, var2) 101 | 102 | def check_path(self, path): 103 | # Let's loop until we either get a working path, or no changes 104 | test_path = path 105 | last_path = None 106 | while True: 107 | # Bail if we've looped at least once and the path didn't change 108 | if last_path != None and last_path == test_path: return None 109 | last_path = test_path 110 | # Check if we stripped everything out 111 | if not len(test_path): return None 112 | # Check if we have a valid path 113 | if os.path.exists(test_path): 114 | return os.path.abspath(test_path) 115 | # Check for quotes 116 | if test_path[0] == test_path[-1] and test_path[0] in ('"',"'"): 117 | test_path = test_path[1:-1] 118 | continue 119 | # Check for a tilde and expand if needed 120 | if test_path[0] == "~": 121 | tilde_expanded = os.path.expanduser(test_path) 122 | if tilde_expanded != test_path: 123 | # Got a change 124 | test_path = tilde_expanded 125 | continue 126 | # Let's check for spaces - strip from the left first, then the right 127 | if test_path[0] in (" ","\t"): 128 | test_path = test_path[1:] 129 | continue 130 | if test_path[-1] in (" ","\t"): 131 | test_path = test_path[:-1] 132 | continue 133 | # Maybe we have escapes to handle? 134 | test_path = "\\".join([x.replace("\\", "") for x in test_path.split("\\\\")]) 135 | 136 | def grab(self, prompt, **kwargs): 137 | # Takes a prompt, a default, and a timeout and shows it with that timeout 138 | # returning the result 139 | timeout = kwargs.get("timeout", 0) 140 | default = kwargs.get("default", None) 141 | # If we don't have a timeout - then skip the timed sections 142 | if timeout <= 0: 143 | if sys.version_info >= (3, 0): 144 | return input(prompt) 145 | else: 146 | return str(raw_input(prompt)) 147 | # Write our prompt 148 | sys.stdout.write(prompt) 149 | sys.stdout.flush() 150 | if os.name == "nt": 151 | start_time = time.time() 152 | i = '' 153 | while True: 154 | if msvcrt.kbhit(): 155 | c = msvcrt.getche() 156 | if ord(c) == 13: # enter_key 157 | break 158 | elif ord(c) >= 32: #space_char 159 | i += c 160 | if len(i) == 0 and (time.time() - start_time) > timeout: 161 | break 162 | else: 163 | i, o, e = select.select( [sys.stdin], [], [], timeout ) 164 | if i: 165 | i = sys.stdin.readline().strip() 166 | print('') # needed to move to next line 167 | if len(i) > 0: 168 | return i 169 | else: 170 | return default 171 | 172 | def cls(self): 173 | os.system('cls' if os.name=='nt' else 'clear') 174 | 175 | def cprint(self, message, **kwargs): 176 | strip_colors = kwargs.get("strip_colors", False) 177 | if os.name == "nt": 178 | strip_colors = True 179 | reset = u"\u001b[0m" 180 | # Requires sys import 181 | for c in self.colors: 182 | if strip_colors: 183 | message = message.replace(c["find"], "") 184 | else: 185 | message = message.replace(c["find"], c["replace"]) 186 | if strip_colors: 187 | return message 188 | sys.stdout.write(message) 189 | print(reset) 190 | 191 | # Needs work to resize the string if color chars exist 192 | '''# Header drawing method 193 | def head(self, text = None, width = 55): 194 | if text == None: 195 | text = self.name 196 | self.cls() 197 | print(" {}".format("#"*width)) 198 | len_text = self.cprint(text, strip_colors=True) 199 | mid_len = int(round(width/2-len(len_text)/2)-2) 200 | middle = " #{}{}{}#".format(" "*mid_len, len_text, " "*((width - mid_len - len(len_text))-2)) 201 | if len(middle) > width+1: 202 | # Get the difference 203 | di = len(middle) - width 204 | # Add the padding for the ...# 205 | di += 3 206 | # Trim the string 207 | middle = middle[:-di] 208 | newlen = len(middle) 209 | middle += "...#" 210 | find_list = [ c["find"] for c in self.colors ] 211 | 212 | # Translate colored string to len 213 | middle = middle.replace(len_text, text + self.rt_color) # always reset just in case 214 | self.cprint(middle) 215 | print("#"*width)''' 216 | 217 | # Header drawing method 218 | def head(self, text = None, width = 55): 219 | if text == None: 220 | text = self.name 221 | self.cls() 222 | print(" {}".format("#"*width)) 223 | mid_len = int(round(width/2-len(text)/2)-2) 224 | middle = " #{}{}{}#".format(" "*mid_len, text, " "*((width - mid_len - len(text))-2)) 225 | if len(middle) > width+1: 226 | # Get the difference 227 | di = len(middle) - width 228 | # Add the padding for the ...# 229 | di += 3 230 | # Trim the string 231 | middle = middle[:-di] + "...#" 232 | print(middle) 233 | print("#"*width) 234 | 235 | def resize(self, width, height): 236 | print('\033[8;{};{}t'.format(height, width)) 237 | 238 | def custom_quit(self): 239 | self.head() 240 | print("by CorpNewt\n") 241 | print("Thanks for testing it out, for bugs/comments/complaints") 242 | print("send me a message on Reddit, or check out my GitHub:\n") 243 | print("www.reddit.com/u/corpnewt") 244 | print("www.github.com/corpnewt\n") 245 | # Get the time and wish them a good morning, afternoon, evening, and night 246 | hr = datetime.datetime.now().time().hour 247 | if hr > 3 and hr < 12: 248 | print("Have a nice morning!\n\n") 249 | elif hr >= 12 and hr < 17: 250 | print("Have a nice afternoon!\n\n") 251 | elif hr >= 17 and hr < 21: 252 | print("Have a nice evening!\n\n") 253 | else: 254 | print("Have a nice night!\n\n") 255 | exit(0) 256 | -------------------------------------------------------------------------------- /gibMacOS.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enableDelayedExpansion 3 | 4 | REM Setup initial vars 5 | set "script_name=" 6 | set "thisDir=%~dp0" 7 | set /a tried=0 8 | set "toask=yes" 9 | set "pause_on_error=yes" 10 | set "py2v=" 11 | set "py2path=" 12 | set "py3v=" 13 | set "py3path=" 14 | set "pypath=" 15 | 16 | REM use_py3: 17 | REM TRUE = Use if found, use py2 otherwise 18 | REM FALSE = Use py2 19 | REM FORCE = Use py3 20 | set "use_py3=TRUE" 21 | 22 | REM Get the system32 (or equivalent) path 23 | call :getsyspath "syspath" 24 | 25 | goto checkscript 26 | 27 | :checkscript 28 | REM Check for our script first 29 | set "looking_for=!script_name!" 30 | if "!script_name!" == "" ( 31 | set "looking_for=%~n0.py or %~n0.command" 32 | set "script_name=%~n0.py" 33 | if not exist "!thisDir!\!script_name!" ( 34 | set "script_name=%~n0.command" 35 | ) 36 | ) 37 | if not exist "!thisDir!\!script_name!" ( 38 | echo Could not find !looking_for!. 39 | echo Please make sure to run this script from the same directory 40 | echo as !looking_for!. 41 | echo. 42 | echo Press [enter] to quit. 43 | pause > nul 44 | exit /b 45 | ) 46 | goto checkpy 47 | 48 | :getsyspath 49 | REM Helper method to return the "proper" path to cmd.exe, reg.exe, and where.exe by walking the ComSpec var 50 | REM Prep the LF variable to use the "line feed" approach 51 | (SET LF=^ 52 | %=this line is empty=% 53 | ) 54 | REM Strip double semi-colons 55 | call :undouble "ComSpec" "%ComSpec%" ";" 56 | set "testpath=%ComSpec:;=!LF!%" 57 | REM Let's walk each path and test if cmd.exe, reg.exe, and where.exe exist there 58 | set /a found=0 59 | for /f "tokens=* delims=" %%i in ("!testpath!") do ( 60 | REM Only continue if we haven't found it yet 61 | if NOT "%%i" == "" ( 62 | if !found! lss 1 ( 63 | set "temppath=%%i" 64 | REM Remove "cmd.exe" from the end if it exists 65 | if /i "!temppath:~-7!" == "cmd.exe" ( 66 | set "temppath=!temppath:~0,-7!" 67 | ) 68 | REM Pad the end with a backslash if needed 69 | if NOT "!temppath:~-1!" == "\" ( 70 | set "temppath=!temppath!\" 71 | ) 72 | REM Let's see if cmd, reg, and where exist there - and set it if so 73 | if EXIST "!temppath!cmd.exe" ( 74 | if EXIST "!temppath!reg.exe" ( 75 | if EXIST "!temppath!where.exe" ( 76 | set /a found=1 77 | set "ComSpec=!temppath!cmd.exe" 78 | set "%~1=!temppath!" 79 | ) 80 | ) 81 | ) 82 | ) 83 | ) 84 | ) 85 | goto :EOF 86 | 87 | :updatepath 88 | set "spath=" 89 | set "upath=" 90 | 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" ) 91 | 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" ) 92 | if not "%spath%" == "" ( 93 | REM We got something in the system path 94 | set "PATH=%spath%" 95 | if not "!upath!" == "" ( 96 | REM We also have something in the user path 97 | set "PATH=%PATH%;%upath%" 98 | ) 99 | ) else if not "%upath%" == "" ( 100 | set "PATH=%upath%" 101 | ) 102 | REM Remove double semicolons from the adjusted PATH 103 | call :undouble "PATH" "%PATH%" ";" 104 | goto :EOF 105 | 106 | :undouble 107 | REM Helper function to strip doubles of a single character out of a string recursively 108 | set "string_value=%~2" 109 | set "check=!string_value:%~3%~3=%~3!" 110 | if not "!check!" == "!string_value!" ( 111 | set "%~1=!check!" 112 | call :undouble "%~1" "!check!" "%~3" 113 | ) 114 | goto :EOF 115 | 116 | :checkpy 117 | call :updatepath 118 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe python 2^> nul`) do ( call :checkpyversion "%%x" "py2v" "py2path" "py3v" "py3path" ) 119 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe python3 2^> nul`) do ( call :checkpyversion "%%x" "py2v" "py2path" "py3v" "py3path" ) 120 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe py 2^> nul`) do ( call :checkpylauncher "%%x" "py2v" "py2path" "py3v" "py3path" ) 121 | set "targetpy=3" 122 | if /i "!use_py3!" == "FALSE" ( 123 | set "targetpy=2" 124 | set "pypath=!py2path!" 125 | ) else if /i "!use_py3!" == "FORCE" ( 126 | set "pypath=!py3path!" 127 | ) else if /i "!use_py3!" == "TRUE" ( 128 | set "pypath=!py3path!" 129 | if "!pypath!" == "" set "pypath=!py2path!" 130 | ) 131 | if not "!pypath!" == "" ( 132 | goto runscript 133 | ) 134 | if !tried! lss 1 ( 135 | if /i "!toask!"=="yes" ( 136 | REM Better ask permission first 137 | goto askinstall 138 | ) else ( 139 | goto installpy 140 | ) 141 | ) else ( 142 | cls 143 | echo ### ### 144 | echo # Warning # 145 | echo ### ### 146 | echo. 147 | REM Couldn't install for whatever reason - give the error message 148 | echo Python is not installed or not found in your PATH var. 149 | echo Please install it from https://www.python.org/downloads/windows/ 150 | echo. 151 | echo Make sure you check the box labeled: 152 | echo. 153 | echo "Add Python X.X to PATH" 154 | echo. 155 | echo Where X.X is the py version you're installing. 156 | echo. 157 | echo Press [enter] to quit. 158 | pause > nul 159 | exit /b 160 | ) 161 | goto runscript 162 | 163 | :checkpylauncher 164 | REM Attempt to check the latest python 2 and 3 versions via the py launcher 165 | for /f "USEBACKQ tokens=*" %%x in (`%~1 -2 -c "import sys; print(sys.executable)" 2^> nul`) do ( call :checkpyversion "%%x" "%~2" "%~3" "%~4" "%~5" ) 166 | for /f "USEBACKQ tokens=*" %%x in (`%~1 -3 -c "import sys; print(sys.executable)" 2^> nul`) do ( call :checkpyversion "%%x" "%~2" "%~3" "%~4" "%~5" ) 167 | goto :EOF 168 | 169 | :checkpyversion 170 | set "version="&for /f "tokens=2* USEBACKQ delims= " %%a in (`"%~1" -V 2^>^&1`) do ( 171 | REM Ensure we have a version number 172 | call :isnumber "%%a" 173 | if not "!errorlevel!" == "0" goto :EOF 174 | set "version=%%a" 175 | ) 176 | if not defined version goto :EOF 177 | if "!version:~0,1!" == "2" ( 178 | REM Python 2 179 | call :comparepyversion "!version!" "!%~2!" 180 | if "!errorlevel!" == "1" ( 181 | set "%~2=!version!" 182 | set "%~3=%~1" 183 | ) 184 | ) else ( 185 | REM Python 3 186 | call :comparepyversion "!version!" "!%~4!" 187 | if "!errorlevel!" == "1" ( 188 | set "%~4=!version!" 189 | set "%~5=%~1" 190 | ) 191 | ) 192 | goto :EOF 193 | 194 | :isnumber 195 | set "var="&for /f "delims=0123456789." %%i in ("%~1") do set var=%%i 196 | if defined var (exit /b 1) 197 | exit /b 0 198 | 199 | :comparepyversion 200 | REM Exits with status 0 if equal, 1 if v1 gtr v2, 2 if v1 lss v2 201 | for /f "tokens=1,2,3 delims=." %%a in ("%~1") do ( 202 | set a1=%%a 203 | set a2=%%b 204 | set a3=%%c 205 | ) 206 | for /f "tokens=1,2,3 delims=." %%a in ("%~2") do ( 207 | set b1=%%a 208 | set b2=%%b 209 | set b3=%%c 210 | ) 211 | if not defined a1 set a1=0 212 | if not defined a2 set a2=0 213 | if not defined a3 set a3=0 214 | if not defined b1 set b1=0 215 | if not defined b2 set b2=0 216 | if not defined b3 set b3=0 217 | if %a1% gtr %b1% exit /b 1 218 | if %a1% lss %b1% exit /b 2 219 | if %a2% gtr %b2% exit /b 1 220 | if %a2% lss %b2% exit /b 2 221 | if %a3% gtr %b3% exit /b 1 222 | if %a3% lss %b3% exit /b 2 223 | exit /b 0 224 | 225 | :askinstall 226 | cls 227 | echo ### ### 228 | echo # Python Not Found # 229 | echo ### ### 230 | echo. 231 | echo Python !targetpy! was not found on the system or in the PATH var. 232 | echo. 233 | set /p "menu=Would you like to install it now? [y/n]: " 234 | if /i "!menu!"=="y" ( 235 | REM We got the OK - install it 236 | goto installpy 237 | ) else if "!menu!"=="n" ( 238 | REM No OK here... 239 | set /a tried=!tried!+1 240 | goto checkpy 241 | ) 242 | REM Incorrect answer - go back 243 | goto askinstall 244 | 245 | :installpy 246 | REM This will attempt to download and install python 247 | REM First we get the html for the python downloads page for Windows 248 | set /a tried=!tried!+1 249 | cls 250 | echo ### ### 251 | echo # Installing Python # 252 | echo ### ### 253 | echo. 254 | echo Gathering info from https://www.python.org/downloads/windows/... 255 | powershell -command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (new-object System.Net.WebClient).DownloadFile('https://www.python.org/downloads/windows/','%TEMP%\pyurl.txt')" 256 | if not exist "%TEMP%\pyurl.txt" ( 257 | goto checkpy 258 | ) 259 | 260 | echo Parsing for latest... 261 | pushd "%TEMP%" 262 | :: Version detection code slimmed by LussacZheng (https://github.com/corpnewt/gibMacOS/issues/20) 263 | for /f "tokens=9 delims=< " %%x in ('findstr /i /c:"Latest Python !targetpy! Release" pyurl.txt') do ( set "release=%%x" ) 264 | popd 265 | 266 | echo Found Python !release! - Downloading... 267 | REM Let's delete our txt file now - we no longer need it 268 | del "%TEMP%\pyurl.txt" 269 | 270 | REM At this point - we should have the version number. 271 | REM We can build the url like so: "https://www.python.org/ftp/python/[version]/python-[version]-amd64.exe" 272 | set "url=https://www.python.org/ftp/python/!release!/python-!release!-amd64.exe" 273 | set "pytype=exe" 274 | if "!targetpy!" == "2" ( 275 | set "url=https://www.python.org/ftp/python/!release!/python-!release!.amd64.msi" 276 | set "pytype=msi" 277 | ) 278 | REM Now we download it with our slick powershell command 279 | powershell -command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (new-object System.Net.WebClient).DownloadFile('!url!','%TEMP%\pyinstall.!pytype!')" 280 | REM If it doesn't exist - we bail 281 | if not exist "%TEMP%\pyinstall.!pytype!" ( 282 | goto checkpy 283 | ) 284 | REM It should exist at this point - let's run it to install silently 285 | echo Installing... 286 | pushd "%TEMP%" 287 | if /i "!pytype!" == "exe" ( 288 | echo pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 289 | pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 290 | ) else ( 291 | set "foldername=!release:.=!" 292 | echo msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR="%LocalAppData%\Programs\Python\Python!foldername:~0,2!" 293 | msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR="%LocalAppData%\Programs\Python\Python!foldername:~0,2!" 294 | ) 295 | popd 296 | echo Installer finished with %ERRORLEVEL% status. 297 | REM Now we should be able to delete the installer and check for py again 298 | del "%TEMP%\pyinstall.!pytype!" 299 | REM If it worked, then we should have python in our PATH 300 | REM this does not get updated right away though - let's try 301 | REM manually updating the local PATH var 302 | call :updatepath 303 | goto checkpy 304 | exit /b 305 | 306 | :runscript 307 | REM Python found 308 | cls 309 | set "args=%*" 310 | set "args=!args:"=!" 311 | if "!args!"=="" ( 312 | "!pypath!" "!thisDir!!script_name!" 313 | ) else ( 314 | "!pypath!" "!thisDir!!script_name!" %* 315 | ) 316 | if /i "!pause_on_error!" == "yes" ( 317 | if not "%ERRORLEVEL%" == "0" ( 318 | echo. 319 | echo Script exited with error code: %ERRORLEVEL% 320 | echo. 321 | echo Press [enter] to exit... 322 | pause > nul 323 | ) 324 | ) 325 | goto :EOF 326 | -------------------------------------------------------------------------------- /gibMacOS.command: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from Scripts import * 3 | import os, datetime, shutil, time, sys, argparse, re 4 | 5 | class gibMacOS: 6 | def __init__(self): 7 | self.d = downloader.Downloader() 8 | self.u = utils.Utils("gibMacOS") 9 | self.r = run.Run() 10 | self.min_w = 80 11 | self.min_h = 24 12 | self.u.resize(self.min_w, self.min_h) 13 | 14 | self.catalog_suffix = { 15 | "public" : "beta", 16 | "publicrelease" : "", 17 | "customer" : "customerseed", 18 | "developer" : "seed" 19 | } 20 | self.current_macos = 15 21 | self.min_macos = 5 22 | self.print_urls = False 23 | self.mac_os_names_url = { 24 | "8" : "mountainlion", 25 | "7" : "lion", 26 | "6" : "snowleopard", 27 | "5" : "leopard" 28 | } 29 | self.version_names = { 30 | "tiger" : "10.4", 31 | "leopard" : "10.5", 32 | "snow leopard" : "10.6", 33 | "lion" : "10.7", 34 | "mountain lion" : "10.8", 35 | "mavericks" : "10.9", 36 | "yosemite" : "10.10", 37 | "el capitan" : "10.11", 38 | "sierra" : "10.12", 39 | "high sierra" : "10.13", 40 | "mojave" : "10.14", 41 | "catalina" : "10.15" 42 | } 43 | self.current_catalog = "publicrelease" 44 | self.catalog_data = None 45 | self.scripts = "Scripts" 46 | self.plist = "sucatalog.plist" 47 | self.saves = "macOS Downloads" 48 | self.save_local = False 49 | self.force_local = False 50 | self.find_recovery = False 51 | self.recovery_suffixes = ( 52 | "RecoveryHDUpdate.pkg", 53 | "RecoveryHDMetaDmg.pkg" 54 | ) 55 | 56 | def resize(self, width=0, height=0): 57 | if os.name=="nt": 58 | # Winders resizing is dumb... bail 59 | return 60 | width = width if width > self.min_w else self.min_w 61 | height = height if height > self.min_h else self.min_h 62 | self.u.resize(width, height) 63 | 64 | def set_prods(self): 65 | self.resize() 66 | if not self.get_catalog_data(self.save_local): 67 | self.u.head("Catalog Data Error") 68 | print("") 69 | print("The currently selected catalog ({}) was not reachable".format(self.current_catalog)) 70 | if self.save_local: 71 | print("and I was unable to locate a valid {} file in the\n{} directory.".format(self.plist, self.scripts)) 72 | print("Please ensure you have a working internet connection.") 73 | print("") 74 | self.u.grab("Press [enter] to exit...") 75 | self.mac_prods = self.get_dict_for_prods(self.get_installers()) 76 | 77 | def set_catalog(self, catalog): 78 | self.current_catalog = catalog.lower() if catalog.lower() in self.catalog_suffix else "publicrelease" 79 | 80 | def build_url(self, **kwargs): 81 | catalog = kwargs.get("catalog", self.current_catalog).lower() 82 | catalog = catalog if catalog.lower() in self.catalog_suffix else "publicrelease" 83 | version = int(kwargs.get("version", self.current_macos)) 84 | url = "https://swscan.apple.com/content/catalogs/others/index-" 85 | url += "-".join([self.mac_os_names_url[str(x)] if str(x) in self.mac_os_names_url else "10."+str(x) for x in reversed(range(self.min_macos, version+1))]) 86 | url += ".merged-1.sucatalog" 87 | ver_s = self.mac_os_names_url[str(version)] if str(version) in self.mac_os_names_url else "10."+str(version) 88 | if len(self.catalog_suffix[catalog]): 89 | url = url.replace(ver_s, ver_s+self.catalog_suffix[catalog]+"-"+ver_s) 90 | return url 91 | 92 | def get_catalog_data(self, local = False): 93 | # Gets the data based on our current_catalog 94 | url = self.build_url(catalog=self.current_catalog, version=self.current_macos) 95 | self.u.head("Downloading Catalog") 96 | print("") 97 | if local: 98 | print("Checking locally for {}".format(self.plist)) 99 | cwd = os.getcwd() 100 | os.chdir(os.path.dirname(os.path.realpath(__file__))) 101 | if os.path.exists(os.path.join(os.path.dirname(os.path.realpath(__file__)), self.scripts, self.plist)): 102 | print(" - Found - loading...") 103 | try: 104 | with open(os.path.join(os.getcwd(), self.scripts, self.plist), "rb") as f: 105 | self.catalog_data = plist.load(f) 106 | os.chdir(cwd) 107 | return True 108 | except: 109 | print(" - Error loading - downloading instead...\n") 110 | os.chdir(cwd) 111 | else: 112 | print(" - Not found - downloading instead...\n") 113 | print("Currently downloading {} catalog from\n\n{}\n".format(self.current_catalog, url)) 114 | try: 115 | b = self.d.get_bytes(url) 116 | print("") 117 | self.catalog_data = plist.loads(b) 118 | except: 119 | print("Error downloading!") 120 | return False 121 | try: 122 | # Assume it's valid data - dump it to a local file 123 | if local or self.force_local: 124 | print(" - Saving to {}...".format(self.plist)) 125 | cwd = os.getcwd() 126 | os.chdir(os.path.dirname(os.path.realpath(__file__))) 127 | with open(os.path.join(os.getcwd(), self.scripts, self.plist), "wb") as f: 128 | plist.dump(self.catalog_data, f) 129 | os.chdir(cwd) 130 | except: 131 | print(" - Error saving!") 132 | return False 133 | return True 134 | 135 | def get_installers(self, plist_dict = None): 136 | if not plist_dict: 137 | plist_dict = self.catalog_data 138 | if not plist_dict: 139 | return [] 140 | mac_prods = [] 141 | for p in plist_dict.get("Products", {}): 142 | if not self.find_recovery: 143 | val = plist_dict.get("Products",{}).get(p,{}).get("ExtendedMetaInfo",{}).get("InstallAssistantPackageIdentifiers",{}) 144 | if val.get("OSInstall",{}) == "com.apple.mpkg.OSInstall" or val.get("SharedSupport","").startswith("com.apple.pkg.InstallAssistant"): 145 | mac_prods.append(p) 146 | else: 147 | # Find out if we have any of the recovery_suffixes 148 | if any(x for x in plist_dict.get("Products",{}).get(p,{}).get("Packages",[]) if x["URL"].endswith(self.recovery_suffixes)): 149 | mac_prods.append(p) 150 | return mac_prods 151 | 152 | def get_build_version(self, dist_dict): 153 | build = version = name = "Unknown" 154 | try: 155 | dist_url = dist_dict.get("English","") if dist_dict.get("English",None) else dist_dict.get("en","") 156 | dist_file = self.d.get_bytes(dist_url, False).decode("utf-8") 157 | except: 158 | dist_file = "" 159 | pass 160 | build_search = "macOSProductBuildVersion" if "macOSProductBuildVersion" in dist_file else "BUILD" 161 | vers_search = "macOSProductVersion" if "macOSProductVersion" in dist_file else "VERSION" 162 | try: 163 | build = dist_file.split("{}".format(build_search))[1].split("")[1].split("")[0] 164 | except: 165 | pass 166 | try: 167 | version = dist_file.split("{}".format(vers_search))[1].split("")[1].split("")[0] 168 | except: 169 | pass 170 | try: 171 | name = re.search(r"(.+?)",dist_file).group(1) 172 | except: 173 | pass 174 | return (build,version,name) 175 | 176 | def get_dict_for_prods(self, prods, plist_dict = None): 177 | if plist_dict==self.catalog_data==None: 178 | plist_dict = {} 179 | else: 180 | plist_dict = self.catalog_data if plist_dict == None else plist_dict 181 | 182 | prod_list = [] 183 | for prod in prods: 184 | # Grab the ServerMetadataURL for the passed product key if it exists 185 | prodd = {"product":prod} 186 | try: 187 | b = self.d.get_bytes(plist_dict.get("Products",{}).get(prod,{}).get("ServerMetadataURL",""), False) 188 | smd = plist.loads(b) 189 | except: 190 | smd = {} 191 | # Populate some info! 192 | prodd["date"] = plist_dict.get("Products",{}).get(prod,{}).get("PostDate","") 193 | prodd["installer"] = False 194 | if plist_dict.get("Products",{}).get(prod,{}).get("ExtendedMetaInfo",{}).get("InstallAssistantPackageIdentifiers",{}).get("OSInstall",{}) == "com.apple.mpkg.OSInstall": 195 | prodd["installer"] = True 196 | prodd["time"] = time.mktime(prodd["date"].timetuple()) + prodd["date"].microsecond / 1E6 197 | prodd["title"] = smd.get("localization",{}).get("English",{}).get("title","Unknown") 198 | prodd["version"] = smd.get("CFBundleShortVersionString","Unknown") 199 | if prodd["version"] == " ": 200 | prodd["version"] = "" 201 | # Try to get the description too 202 | try: 203 | desc = smd.get("localization",{}).get("English",{}).get("description","").decode("utf-8") 204 | desctext = desc.split('"p1">')[1].split("")[0] 205 | except: 206 | desctext = None 207 | prodd["description"] = desctext 208 | # Iterate the available packages and save their urls and sizes 209 | if self.find_recovery: 210 | # Only get the recovery packages 211 | prodd["packages"] = [x for x in plist_dict.get("Products",{}).get(prod,{}).get("Packages",[]) if x["URL"].endswith(self.recovery_suffixes)] 212 | else: 213 | # Add them all! 214 | prodd["packages"] = plist_dict.get("Products",{}).get(prod,{}).get("Packages",[]) 215 | # Get size 216 | prodd["size"] = 0 217 | for i in prodd["packages"]: prodd["size"] += i["Size"] 218 | prodd["size"] = self.d.get_size(prodd["size"]) 219 | # Attempt to get the build/version info from the dist 220 | b,v,n = self.get_build_version(plist_dict.get("Products",{}).get(prod,{}).get("Distributions",{})) 221 | prodd["title"] = smd.get("localization",{}).get("English",{}).get("title",n) 222 | prodd["build"] = b 223 | if not v.lower() == "unknown": 224 | prodd["version"] = v 225 | prod_list.append(prodd) 226 | # Sort by newest 227 | prod_list = sorted(prod_list, key=lambda x:x["time"], reverse=True) 228 | return prod_list 229 | 230 | def download_prod(self, prod, dmg = False): 231 | # Takes a dictonary of details and downloads it 232 | self.resize() 233 | name = "{} - {} {}".format(prod["product"], prod["version"], prod["title"]).replace(":","").strip() 234 | dl_list = [] 235 | for x in prod["packages"]: 236 | if not x.get("URL",None): 237 | continue 238 | if dmg and not x.get("URL","").lower().endswith(".dmg"): 239 | continue 240 | # add it to the list 241 | dl_list.append(x["URL"]) 242 | if not len(dl_list): 243 | self.u.head("Error") 244 | print("") 245 | print("There were no files to download") 246 | print("") 247 | self.u.grab("Press [enter] to return...") 248 | return 249 | c = 0 250 | done = [] 251 | if self.print_urls: 252 | self.u.head("Download Links") 253 | print("") 254 | print("{}:\n".format(name)) 255 | print("\n".join([" - {} \n --> {}".format(os.path.basename(x), x) for x in dl_list])) 256 | print("") 257 | self.u.grab("Press [enter] to return...") 258 | return 259 | # Only check the dirs if we need to 260 | cwd = os.getcwd() 261 | os.chdir(os.path.dirname(os.path.realpath(__file__))) 262 | if os.path.exists(os.path.join(os.getcwd(), self.saves, self.current_catalog, name)): 263 | while True: 264 | self.u.head("Already Exists") 265 | print("") 266 | print("It looks like you've already downloaded {}".format(name)) 267 | print("") 268 | menu = self.u.grab("Redownload? (y/n): ") 269 | if not len(menu): 270 | continue 271 | if menu[0].lower() == "n": 272 | return 273 | if menu[0].lower() == "y": 274 | break 275 | # Remove the old copy, then re-download 276 | shutil.rmtree(os.path.join(os.getcwd(), self.saves, self.current_catalog, name)) 277 | # Make it new 278 | os.makedirs(os.path.join(os.getcwd(), self.saves, self.current_catalog, name)) 279 | for x in dl_list: 280 | c += 1 281 | self.u.head("Downloading File {} of {}".format(c, len(dl_list))) 282 | print("") 283 | if len(done): 284 | print("\n".join(["{} --> {}".format(y["name"], "Succeeded" if y["status"] else "Failed") for y in done])) 285 | print("") 286 | if dmg: 287 | print("NOTE: Only Downloading DMG Files") 288 | print("") 289 | print("Downloading {} for {}...".format(os.path.basename(x), name)) 290 | print("") 291 | try: 292 | self.d.stream_to_file(x, os.path.join(os.getcwd(), self.saves, self.current_catalog, name, os.path.basename(x))) 293 | done.append({"name":os.path.basename(x), "status":True}) 294 | except: 295 | done.append({"name":os.path.basename(x), "status":False}) 296 | succeeded = [x for x in done if x["status"]] 297 | failed = [x for x in done if not x["status"]] 298 | self.u.head("Downloaded {} of {}".format(len(succeeded), len(dl_list))) 299 | print("") 300 | print("Succeeded:") 301 | if len(succeeded): 302 | for x in succeeded: 303 | print(" {}".format(x["name"])) 304 | else: 305 | print(" None") 306 | print("") 307 | print("Failed:") 308 | if len(failed): 309 | for x in failed: 310 | print(" {}".format(x["name"])) 311 | else: 312 | print(" None") 313 | print("") 314 | print("Files saved to:") 315 | print(" {}".format(os.path.join(os.getcwd(), self.saves, self.current_catalog, name))) 316 | print("") 317 | self.u.grab("Press [enter] to return...") 318 | 319 | def show_catalog_url(self): 320 | self.resize() 321 | self.u.head() 322 | print("") 323 | print("Current Catalog: {}".format(self.current_catalog)) 324 | print("Max macOS Version: 10.{}".format(self.current_macos)) 325 | print("") 326 | print("{}".format(self.build_url())) 327 | print("") 328 | menu = self.u.grab("Press [enter] to return...") 329 | return 330 | 331 | def pick_catalog(self): 332 | self.resize() 333 | self.u.head("Select SU Catalog") 334 | print("") 335 | count = 0 336 | for x in self.catalog_suffix: 337 | count += 1 338 | print("{}. {}".format(count, x)) 339 | print("") 340 | print("M. Main Menu") 341 | print("Q. Quit") 342 | print("") 343 | menu = self.u.grab("Please select an option: ") 344 | if not len(menu): 345 | self.pick_catalog() 346 | return 347 | if menu[0].lower() == "m": 348 | return 349 | elif menu[0].lower() == "q": 350 | self.u.custom_quit() 351 | # Should have something to test here 352 | try: 353 | i = int(menu) 354 | self.current_catalog = list(self.catalog_suffix)[i-1] 355 | except: 356 | # Incorrect - try again 357 | self.pick_catalog() 358 | return 359 | # If we made it here - then we got something 360 | # Reload with the proper catalog 361 | self.get_catalog_data() 362 | 363 | def pick_macos(self): 364 | self.resize() 365 | self.u.head("Select Max macOS Version") 366 | print("") 367 | print("Currently set to 10.{}".format(self.current_macos)) 368 | print("") 369 | print("M. Main Menu") 370 | print("Q. Quit") 371 | print("") 372 | menu = self.u.grab("Please type the max macOS version for the catalog url (10.xx format): ") 373 | if not len(menu): 374 | self.pick_macos() 375 | return 376 | if menu[0].lower() == "m": 377 | return 378 | elif menu[0].lower() == "q": 379 | self.u.custom_quit() 380 | # At this point - we should have something in 10.xx format 381 | parts = menu.split(".") 382 | if len(parts) > 2 or len(parts) < 2 or parts[0] != "10": 383 | self.pick_macos() 384 | return 385 | # Got the right format 386 | try: 387 | self.current_macos = int(parts[1]) 388 | except: 389 | # Not an int 390 | self.pick_macos() 391 | return 392 | # At this point, we should be good 393 | self.get_catalog_data() 394 | 395 | def main(self, dmg = False): 396 | self.u.head() 397 | print("") 398 | print("Available Products:") 399 | print("") 400 | num = 0 401 | w = 0 402 | pad = 12 403 | if not len(self.mac_prods): 404 | print("No installers in catalog!") 405 | print("") 406 | for p in self.mac_prods: 407 | num += 1 408 | var1 = "{}. {} {}".format(num, p["title"], p["version"]) 409 | if p["build"].lower() != "unknown": 410 | var1 += " ({})".format(p["build"]) 411 | var2 = " - {} - Added {} - {}".format(p["product"], p["date"], p["size"]) 412 | if self.find_recovery and p["installer"]: 413 | # Show that it's a full installer 414 | var2 += " - FULL Install" 415 | w = len(var1) if len(var1) > w else w 416 | w = len(var2) if len(var2) > w else w 417 | print(var1) 418 | print(var2) 419 | print("") 420 | print("M. Change Max-OS Version (Currently 10.{})".format(self.current_macos)) 421 | print("C. Change Catalog (Currently {})".format(self.current_catalog)) 422 | print("I. Only Print URLs (Currently {})".format(self.print_urls)) 423 | if sys.platform.lower() == "darwin": 424 | pad += 2 425 | print("S. Set Current Catalog to SoftwareUpdate Catalog") 426 | print("L. Clear SoftwareUpdate Catalog") 427 | print("R. Toggle Recovery-Only (Currently {})".format("On" if self.find_recovery else "Off")) 428 | print("U. Show Catalog URL") 429 | print("Q. Quit") 430 | self.resize(w, (num*2)+pad) 431 | if os.name=="nt": 432 | # Formatting differences.. 433 | print("") 434 | menu = self.u.grab("Please select an option: ") 435 | if not len(menu): 436 | return 437 | if menu[0].lower() == "q": 438 | self.resize() 439 | self.u.custom_quit() 440 | elif menu[0].lower() == "u": 441 | self.show_catalog_url() 442 | return 443 | elif menu[0].lower() == "m": 444 | self.pick_macos() 445 | elif menu[0].lower() == "c": 446 | self.pick_catalog() 447 | elif menu[0].lower() == "i": 448 | self.print_urls ^= True 449 | return 450 | elif menu[0].lower() == "l" and sys.platform.lower() == "darwin": 451 | # Clear the software update catalog 452 | self.u.head("Clearing SU CatalogURL") 453 | print("") 454 | print("sudo softwareupdate --clear-catalog") 455 | self.r.run({"args":["softwareupdate","--clear-catalog"],"sudo":True}) 456 | print("") 457 | self.u.grab("Done.", timeout=5) 458 | return 459 | elif menu[0].lower() == "s" and sys.platform.lower() == "darwin": 460 | # Set the software update catalog to our current catalog url 461 | self.u.head("Setting SU CatalogURL") 462 | print("") 463 | url = self.build_url(catalog=self.current_catalog, version=self.current_macos) 464 | print("Setting catalog URL to:\n{}".format(url)) 465 | print("") 466 | print("sudo softwareupdate --set-catalog {}".format(url)) 467 | self.r.run({"args":["softwareupdate","--set-catalog",url],"sudo":True}) 468 | print("") 469 | self.u.grab("Done",timeout=5) 470 | return 471 | elif menu[0].lower() == "r": 472 | self.find_recovery ^= True 473 | if menu[0].lower() in ["m","c","r"]: 474 | self.u.head("Parsing Data") 475 | print("") 476 | print("Re-scanning products after url preference toggled...") 477 | self.mac_prods = self.get_dict_for_prods(self.get_installers()) 478 | return 479 | 480 | # Assume we picked something 481 | try: 482 | menu = int(menu) 483 | except: 484 | return 485 | if menu < 1 or menu > len(self.mac_prods): 486 | return 487 | self.download_prod(self.mac_prods[menu-1], dmg) 488 | 489 | def get_latest(self, dmg = False): 490 | self.u.head("Downloading Latest") 491 | print("") 492 | self.download_prod(sorted(self.mac_prods, key=lambda x:x['version'], reverse=True)[0], dmg) 493 | 494 | def get_for_product(self, prod, dmg = False): 495 | self.u.head("Downloading for {}".format(prod)) 496 | print("") 497 | for p in self.mac_prods: 498 | if p["product"] == prod: 499 | self.download_prod(p, dmg) 500 | return 501 | print("{} not found".format(prod)) 502 | 503 | def get_for_version(self, vers, dmg = False): 504 | self.u.head("Downloading for {}".format(vers)) 505 | print("") 506 | # Map the versions to their names 507 | v = self.version_names.get(vers.lower(),vers.lower()) 508 | v_dict = {} 509 | for n in self.version_names: 510 | v_dict[self.version_names[n]] = n 511 | n = v_dict.get(v, v) 512 | for p in sorted(self.mac_prods, key=lambda x:x['version'], reverse=True): 513 | pt = p["title"].lower() 514 | pv = p["version"].lower() 515 | # Need to compare verisons - n = name, v = version 516 | # p["version"] and p["title"] may contain either the version 517 | # or name - so check both 518 | # We want to make sure, if we match the name to the title, that we only match 519 | # once - so Sierra/High Sierra don't cross-match 520 | # 521 | # First check if p["version"] isn't " " or "1.0" 522 | if not pv in [" ","1.0"]: 523 | # Have a real version - match this first 524 | if pv.startswith(v): 525 | self.download_prod(p, dmg) 526 | return 527 | # Didn't match the version - or version was bad, let's check 528 | # the title 529 | # Need to make sure n is in the version name, but not equal to it, 530 | # and the version name is in p["title"] to disqualify 531 | # i.e. - "Sierra" exists in "High Sierra", but does not equal "High Sierra" 532 | # and "High Sierra" is in "macOS High Sierra 10.13.6" - This would match 533 | name_match = [x for x in self.version_names if n in x and x != n and x in pt] 534 | if (n in pt) and not len(name_match): 535 | self.download_prod(p, dmg) 536 | return 537 | print("'{}' not found".format(vers)) 538 | 539 | if __name__ == '__main__': 540 | parser = argparse.ArgumentParser() 541 | parser.add_argument("-l", "--latest", help="downloads the version available in the current catalog (overrides --version and --product)", action="store_true") 542 | 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") 543 | parser.add_argument("-d", "--dmg", help="downloads only the .dmg files", action="store_true") 544 | parser.add_argument("-s", "--savelocal", help="uses a locally saved sucatalog.plist if exists", action="store_true") 545 | parser.add_argument("-n", "--newlocal", help="downloads and saves locally, overwriting any prior local sucatalog.plist", action="store_true") 546 | parser.add_argument("-c", "--catalog", help="sets the CATALOG to use - publicrelease, public, customer, developer") 547 | parser.add_argument("-p", "--product", help="sets the product id to search for (overrides --version)") 548 | parser.add_argument("-v", "--version", help="sets the version of macOS to target - eg '-v 10.14' or '-v Yosemite'") 549 | parser.add_argument("-m", "--maxos", help="sets the max macOS version to consider when building the url - eg 10.14") 550 | parser.add_argument("-i", "--print-urls", help="only prints the download URLs, does not actually download them", action="store_true") 551 | args = parser.parse_args() 552 | 553 | g = gibMacOS() 554 | if args.recovery: 555 | args.dmg = False 556 | g.find_recovery = args.recovery 557 | 558 | if args.savelocal: 559 | g.save_local = True 560 | 561 | if args.newlocal: 562 | g.force_local = True 563 | 564 | if args.print_urls: 565 | g.print_urls = True 566 | 567 | if args.maxos: 568 | try: 569 | m = int(str(args.maxos).replace("10.","")) 570 | g.current_macos = m 571 | except: 572 | pass 573 | if args.catalog: 574 | # Set the catalog 575 | g.set_catalog(args.catalog) 576 | 577 | # Done setting up pre-requisites 578 | g.set_prods() 579 | 580 | if args.latest: 581 | g.get_latest(args.dmg) 582 | exit() 583 | if args.product != None: 584 | g.get_for_product(args.product, args.dmg) 585 | exit() 586 | if args.version != None: 587 | g.get_for_version(args.version, args.dmg) 588 | exit() 589 | 590 | while True: 591 | g.main(args.dmg) 592 | --------------------------------------------------------------------------------