[
  {
    "path": "BuildmacOSInstallApp.command",
    "content": "#!/usr/bin/env bash\n\n# Get the curent directory, the script name\n# and the script name with \"py\" substituted for the extension.\nargs=( \"$@\" )\ndir=\"$(cd -- \"$(dirname \"$0\")\" >/dev/null 2>&1; pwd -P)\"\nscript=\"${0##*/}\"\ntarget=\"${script%.*}.py\"\n\n# use_py3:\n#   TRUE  = Use if found, use py2 otherwise\n#   FALSE = Use py2\n#   FORCE = Use py3\nuse_py3=\"TRUE\"\n\n# We'll parse if the first argument passed is\n# --install-python and if so, we'll just install\n# Can optionally take a version number as the\n# second arg - i.e. --install-python 3.13.1\njust_installing=\"FALSE\"\n\ntempdir=\"\"\n\ncompare_to_version () {\n    # Compares our OS version to the passed OS version, and\n    # return a 1 if we match the passed compare type, or a 0 if we don't.\n    # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal)\n    # $2 = OS version to compare ours to\n    if [ -z \"$1\" ] || [ -z \"$2\" ]; then\n        # Missing info - bail.\n        return\n    fi\n    local current_os= comp=\n    current_os=\"$(sw_vers -productVersion 2>/dev/null)\"\n    comp=\"$(vercomp \"$current_os\" \"$2\")\"\n    # Check gequal and lequal first\n    if [[ \"$1\" == \"3\" && (\"$comp\" == \"1\" || \"$comp\" == \"0\") ]] || [[ \"$1\" == \"4\" && (\"$comp\" == \"2\" || \"$comp\" == \"0\") ]] || [[ \"$comp\" == \"$1\" ]]; then\n        # Matched\n        echo \"1\"\n    else\n        # No match\n        echo \"0\"\n    fi\n}\n\nset_use_py3_if () {\n    # Auto sets the \"use_py3\" variable based on\n    # conditions passed\n    # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal)\n    # $2 = OS version to compare\n    # $3 = TRUE/FALSE/FORCE in case of match\n    if [ -z \"$1\" ] || [ -z \"$2\" ] || [ -z \"$3\" ]; then\n        # Missing vars - bail with no changes.\n        return\n    fi\n    if [ \"$(compare_to_version \"$1\" \"$2\")\" == \"1\" ]; then\n        use_py3=\"$3\"\n    fi\n}\n\nget_remote_py_version () {\n    local pyurl= py_html= py_vers= py_num=\"3\"\n    pyurl=\"https://www.python.org/downloads/macos/\"\n    py_html=\"$(curl -L $pyurl --compressed 2>&1)\"\n    if [ -z \"$use_py3\" ]; then\n        use_py3=\"TRUE\"\n    fi\n    if [ \"$use_py3\" == \"FALSE\" ]; then\n        py_num=\"2\"\n    fi\n    py_vers=\"$(echo \"$py_html\" | grep -i \"Latest Python $py_num Release\" | awk '{print $8}' | cut -d'<' -f1)\"\n    echo \"$py_vers\"\n}\n\ndownload_py () {\n    local vers=\"$1\" url=\n    clear\n    echo \"  ###                        ###\"\n    echo \" #     Downloading Python     #\"\n    echo \"###                        ###\"\n    echo\n    if [ -z \"$vers\" ]; then\n        echo \"Gathering latest version...\"\n        vers=\"$(get_remote_py_version)\"\n        if [ -z \"$vers\" ]; then\n            if [ \"$just_installing\" == \"TRUE\" ]; then\n                echo \" - Failed to get info!\"\n                exit 1\n            else\n                # Didn't get it still - bail\n                print_error\n            fi\n        fi\n        echo \"Located Version:  $vers\"\n    else\n        # Got a version passed\n        echo \"User-Provided Version:  $vers\"\n    fi\n    echo \"Building download url...\"\n    url=\"$(\\\n    curl -L https://www.python.org/downloads/release/python-${vers//./}/ --compressed 2>&1 | \\\n    grep -iE \"python-$vers-macos.*.pkg\\\"\" | \\\n    grep -iE \"a href=\" | \\\n    awk -F'\"' '{ print $2 }' | \\\n    head -n 1\\\n    )\"\n    if [ -z \"$url\" ]; then\n        if [ \"$just_installing\" == \"TRUE\" ]; then\n            echo \" - Failed to build download url!\"\n            exit 1\n        else\n            # Couldn't get the URL - bail\n            print_error\n        fi\n    fi\n    echo \" - $url\"\n    echo \"Downloading...\"\n    # Create a temp dir and download to it\n    tempdir=\"$(mktemp -d 2>/dev/null || mktemp -d -t 'tempdir')\"\n    curl \"$url\" -o \"$tempdir/python.pkg\"\n    if [ \"$?\" != \"0\" ]; then\n        echo \" - Failed to download python installer!\"\n        exit $?\n    fi\n    echo\n    echo \"Running python install package...\"\n    echo\n    sudo installer -pkg \"$tempdir/python.pkg\" -target /\n    if [ \"$?\" != \"0\" ]; then\n        echo \" - Failed to install python!\"\n        exit $?\n    fi\n    echo\n    # Now we expand the package and look for a shell update script\n    pkgutil --expand \"$tempdir/python.pkg\" \"$tempdir/python\"\n    if [ -e \"$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall\" ]; then\n        # Run the script\n        echo \"Updating PATH...\"\n        echo\n        \"$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall\"\n        echo\n    fi\n    vers_folder=\"Python $(echo \"$vers\" | cut -d'.' -f1 -f2)\"\n    if [ -f \"/Applications/$vers_folder/Install Certificates.command\" ]; then\n        # Certs script exists - let's execute that to make sure our certificates are updated\n        echo \"Updating Certificates...\"\n        echo\n        \"/Applications/$vers_folder/Install Certificates.command\"\n        echo\n    fi\n    echo \"Cleaning up...\"\n    cleanup\n    if [ \"$just_installing\" == \"TRUE\" ]; then\n        echo\n        echo \"Done.\"\n    else\n        # Now we check for py again\n        downloaded=\"TRUE\"\n        clear\n        main\n    fi\n}\n\ncleanup () {\n    if [ -d \"$tempdir\" ]; then\n        rm -Rf \"$tempdir\"\n    fi\n}\n\nprint_error() {\n    clear\n    cleanup\n    echo \"  ###                      ###\"\n    echo \" #     Python Not Found     #\"\n    echo \"###                      ###\"\n    echo\n    echo \"Python is not installed or not found in your PATH var.\"\n    echo\n    if [ \"$kernel\" == \"Darwin\" ]; then\n        echo \"Please go to https://www.python.org/downloads/macos/ to\"\n        echo \"download and install the latest version, then try again.\"\n    else\n        echo \"Please install python through your package manager and\"\n        echo \"try again.\"\n    fi\n    echo\n    exit 1\n}\n\nprint_target_missing() {\n    clear\n    cleanup\n    echo \"  ###                      ###\"\n    echo \" #     Target Not Found     #\"\n    echo \"###                      ###\"\n    echo\n    echo \"Could not locate $target!\"\n    echo\n    exit 1\n}\n\nformat_version () {\n    local vers=\"$1\"\n    echo \"$(echo \"$1\" | awk -F. '{ printf(\"%d%03d%03d%03d\\n\", $1,$2,$3,$4); }')\"\n}\n\nvercomp () {\n    # Modified from: https://apple.stackexchange.com/a/123408/11374\n    local ver1=\"$(format_version \"$1\")\" ver2=\"$(format_version \"$2\")\"\n    if [ $ver1 -gt $ver2 ]; then\n        echo \"1\"\n    elif [ $ver1 -lt $ver2 ]; then\n        echo \"2\"\n    else\n        echo \"0\"\n    fi\n}\n\nget_local_python_version() {\n    # $1 = Python bin name (defaults to python3)\n    # Echoes the path to the highest version of the passed python bin if any\n    local py_name=\"$1\" max_version= python= python_version= python_path=\n    if [ -z \"$py_name\" ]; then\n        py_name=\"python3\"\n    fi\n    py_list=\"$(which -a \"$py_name\" 2>/dev/null)\"\n    # Walk that newline separated list\n    while read python; do\n        if [ -z \"$python\" ]; then\n            # Got a blank line - skip\n            continue\n        fi\n        if [ \"$check_py3_stub\" == \"1\" ] && [ \"$python\" == \"/usr/bin/python3\" ]; then\n            # See if we have a valid developer path\n            xcode-select -p > /dev/null 2>&1\n            if [ \"$?\" != \"0\" ]; then\n                # /usr/bin/python3 path - but no valid developer dir\n                continue\n            fi\n        fi\n        python_version=\"$(get_python_version $python)\"\n        if [ -z \"$python_version\" ]; then\n            # Didn't find a py version - skip\n            continue\n        fi\n        # Got the py version - compare to our max\n        if [ -z \"$max_version\" ] || [ \"$(vercomp \"$python_version\" \"$max_version\")\" == \"1\" ]; then\n            # Max not set, or less than the current - update it\n            max_version=\"$python_version\"\n            python_path=\"$python\"\n        fi\n    done <<< \"$py_list\"\n    echo \"$python_path\"\n}\n\nget_python_version() {\n    local py_path=\"$1\" py_version=\n    # Get the python version by piping stderr into stdout (for py2), then grepping the output for\n    # the word \"python\", getting the second element, and grepping for an alphanumeric version number\n    py_version=\"$($py_path -V 2>&1 | grep -i python | cut -d' ' -f2 | grep -E \"[A-Za-z\\d\\.]+\")\"\n    if [ ! -z \"$py_version\" ]; then\n        echo \"$py_version\"\n    fi\n}\n\nprompt_and_download() {\n    if [ \"$downloaded\" != \"FALSE\" ] || [ \"$kernel\" != \"Darwin\" ]; then\n        # We already tried to download, or we're not on macOS - just bail\n        print_error\n    fi\n    clear\n    echo \"  ###                      ###\"\n    echo \" #     Python Not Found     #\"\n    echo \"###                      ###\"\n    echo\n    target_py=\"Python 3\"\n    printed_py=\"Python 2 or 3\"\n    if [ \"$use_py3\" == \"FORCE\" ]; then\n        printed_py=\"Python 3\"\n    elif [ \"$use_py3\" == \"FALSE\" ]; then\n        target_py=\"Python 2\"\n        printed_py=\"Python 2\"\n    fi\n    echo \"Could not locate $printed_py!\"\n    echo\n    echo \"This script requires $printed_py to run.\"\n    echo\n    while true; do\n        read -p \"Would you like to install the latest $target_py now? (y/n):  \" yn\n        case $yn in\n            [Yy]* ) download_py;break;;\n            [Nn]* ) print_error;;\n        esac\n    done\n}\n\nmain() {\n    local python= version=\n    # Verify our target exists\n    if [ ! -f \"$dir/$target\" ]; then\n        # Doesn't exist\n        print_target_missing\n    fi\n    if [ -z \"$use_py3\" ]; then\n        use_py3=\"TRUE\"\n    fi\n    if [ \"$use_py3\" != \"FALSE\" ]; then\n        # Check for py3 first\n        python=\"$(get_local_python_version python3)\"\n    fi\n    if [ \"$use_py3\" != \"FORCE\" ] && [ -z \"$python\" ]; then\n        # We aren't using py3 explicitly, and we don't already have a path\n        python=\"$(get_local_python_version python2)\"\n        if [ -z \"$python\" ]; then\n            # Try just looking for \"python\"\n            python=\"$(get_local_python_version python)\"\n        fi\n    fi\n    if [ -z \"$python\" ]; then\n        # Didn't ever find it - prompt\n        prompt_and_download\n        return 1\n    fi\n    # Found it - start our script and pass all args\n    \"$python\" \"$dir/$target\" \"${args[@]}\"\n}\n\n# Keep track of whether or not we're on macOS to determine if\n# we can download and install python for the user as needed.\nkernel=\"$(uname -s)\"\n# Check to see if we need to force based on\n# macOS version. 10.15 has a dummy python3 version\n# that can trip up some py3 detection in other scripts.\n# set_use_py3_if \"3\" \"10.15\" \"FORCE\"\ndownloaded=\"FALSE\"\n# Check for the aforementioned /usr/bin/python3 stub if\n# our OS version is 10.15 or greater.\ncheck_py3_stub=\"$(compare_to_version \"3\" \"10.15\")\"\ntrap cleanup EXIT\nif [ \"$1\" == \"--install-python\" ] && [ \"$kernel\" == \"Darwin\" ]; then\n    just_installing=\"TRUE\"\n    download_py \"$2\"\nelse\n    main\nfi\n"
  },
  {
    "path": "BuildmacOSInstallApp.py",
    "content": "#!/usr/bin/env python\nfrom Scripts import *\nimport os, datetime, shutil, time, sys, argparse\n\n# Using the techniques outlined by wolfmannight here:  https://www.insanelymac.com/forum/topic/338810-create-legit-copy-of-macos-from-apple-catalog/\n\nclass buildMacOSInstallApp:\n    def __init__(self):\n        self.r = run.Run()\n        self.u = utils.Utils(\"Build macOS Install App\")\n        self.target_files = [\n            \"BaseSystem.dmg\",\n            \"BaseSystem.chunklist\",\n            \"InstallESDDmg.pkg\",\n            \"InstallInfo.plist\",\n            \"AppleDiagnostics.dmg\",\n            \"AppleDiagnostics.chunklist\"\n        ]\n        # Verify we're on macOS - this doesn't work anywhere else\n        if not sys.platform == \"darwin\":\n            self.u.head(\"WARNING\")\n            print(\"\")\n            print(\"This script only runs on macOS!\")\n            print(\"\")\n            exit(1)\n\n    def mount_dmg(self, dmg, no_browse = False):\n        # Mounts the passed dmg and returns the mount point(s)\n        args = [\"/usr/bin/hdiutil\", \"attach\", dmg, \"-plist\", \"-noverify\"]\n        if no_browse:\n            args.append(\"-nobrowse\")\n        out = self.r.run({\"args\":args})\n        if out[2] != 0:\n            # Failed!\n            raise Exception(\"Mount Failed!\", \"{} failed to mount:\\n\\n{}\".format(os.path.basename(dmg), out[1]))\n        # Get the plist data returned, and locate the mount points\n        try:\n            plist_data = plist.loads(out[0])\n            mounts = [x[\"mount-point\"] for x in plist_data.get(\"system-entities\", []) if \"mount-point\" in x]\n            return mounts\n        except:\n            raise Exception(\"Mount Failed!\", \"No mount points returned from {}\".format(os.path.basename(dmg)))\n\n    def unmount_dmg(self, mount_point):\n        # Unmounts the passed dmg or mount point - retries with force if failed\n        # Can take either a single point or a list\n        if not type(mount_point) is list:\n            mount_point = [mount_point]\n        unmounted = []\n        for m in mount_point:    \n            args = [\"/usr/bin/hdiutil\", \"detach\", m]\n            out = self.r.run({\"args\":args})\n            if out[2] != 0:\n                # Polite failed, let's crush this b!\n                args.append(\"-force\")\n                out = self.r.run({\"args\":args})\n                if out[2] != 0:\n                    # Oh... failed again... onto the next...\n                    print(out[1])\n                    continue\n            unmounted.append(m)\n        return unmounted\n\n    def main(self):\n        while True:\n            self.u.head()\n            print(\"\")\n            print(\"Q. Quit\")\n            print(\"\")\n            fold = self.u.grab(\"Please drag and drop the output folder from gibMacOS here:  \")\n            print(\"\")\n            if fold.lower() == \"q\":\n                self.u.custom_quit()\n            f_path = self.u.check_path(fold)\n            if not f_path:\n                print(\"That path does not exist!\\n\")\n                self.u.grab(\"Press [enter] to return...\")\n                continue\n            # Let's check if it's a folder.  If not, make the next directory up the target\n            if not os.path.isdir(f_path):\n                f_path = os.path.dirname(os.path.realpath(f_path))\n            # Walk the contents of f_path and ensure we have all the needed files\n            lower_contents = [y.lower() for y in os.listdir(f_path)]\n            # Check if we got an InstallAssistant.pkg - and if so, just open that\n            if \"installassistant.pkg\" in lower_contents:\n                self.u.head(\"InstallAssistant.pkg Found\")\n                print(\"\")\n                print(\"Located InstallAssistant.pkg in the passed folder.\\n\")\n                print(\"As of macOS Big Sur (11.x), Apple changed how they distribute the OS files in\")\n                print(\"the software update catalog.\\n\")\n                print(\"Double clicking the InstallAssistant.pkg will open it in Installer, which will\")\n                print(\"copy the Install macOS [version].app to your /Applications folder.\\n\")\n                print(\"Opening InstallAssistant.pkg...\")\n                self.r.run({\"args\":[\"open\",os.path.join(f_path,\"InstallAssistant.pkg\")]})\n                print(\"\")\n                self.u.grab(\"Press [enter] to return...\")\n                continue\n            missing_list = [x for x in self.target_files if not x.lower() in lower_contents]\n            if len(missing_list):\n                self.u.head(\"Missing Required Files\")\n                print(\"\")\n                print(\"That folder is missing the following required files:\")\n                print(\", \".join(missing_list))\n                print(\"\")\n                self.u.grab(\"Press [enter] to return...\")\n            # Time to build the installer!\n            cwd = os.getcwd()\n            os.chdir(f_path)\n            base_mounts = []\n            try:\n                self.u.head(\"Building Installer\")\n                print(\"\")\n                print(\"Taking ownership of downloaded files...\")\n                for x in self.target_files:\n                    print(\" - {}...\".format(x))\n                    self.r.run({\"args\":[\"chmod\",\"a+x\",x]})\n                print(\"Mounting BaseSystem.dmg...\")\n                base_mounts = self.mount_dmg(\"BaseSystem.dmg\")\n                if not len(base_mounts):\n                    raise Exception(\"Mount Failed!\", \"No mount points were returned from BaseSystem.dmg\")\n                base_mount = base_mounts[0] # Let's assume the first\n                print(\"Locating Installer app...\")\n                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)\n                if not install_app:\n                    raise Exception(\"Installer app not located in {}\".format(base_mount))\n                print(\" - Found {}\".format(install_app))\n                # Copy the .app over\n                out = self.r.run({\"args\":[\"cp\",\"-R\",os.path.join(base_mount,install_app),os.path.join(f_path,install_app)]})\n                if out[2] != 0:\n                    raise Exception(\"Copy Failed!\", out[1])\n                print(\"Unmounting BaseSystem.dmg...\")\n                for x in base_mounts:\n                    self.unmount_dmg(x)\n                base_mounts = []\n                shared_support = os.path.join(f_path,install_app,\"Contents\",\"SharedSupport\")\n                if not os.path.exists(shared_support):\n                    print(\"Creating SharedSupport directory...\")\n                    os.makedirs(shared_support)\n                print(\"Copying files to SharedSupport...\")\n                for x in self.target_files:\n                    y = \"InstallESD.dmg\" if x.lower() == \"installesddmg.pkg\" else x # InstallESDDmg.pkg gets renamed to InstallESD.dmg - all others stay the same\n                    print(\" - {}{}\".format(x, \" --> {}\".format(y) if y != x else \"\"))\n                    out = self.r.run({\"args\":[\"cp\",\"-R\",os.path.join(f_path,x),os.path.join(shared_support,y)]})\n                    if out[2] != 0:\n                        raise Exception(\"Copy Failed!\", out[1])\n                print(\"Patching InstallInfo.plist...\")\n                with open(os.path.join(shared_support,\"InstallInfo.plist\"),\"rb\") as f:\n                    p = plist.load(f)\n                if \"Payload Image Info\" in p:\n                    pii = p[\"Payload Image Info\"]\n                    if \"URL\" in pii: pii[\"URL\"] = pii[\"URL\"].replace(\"InstallESDDmg.pkg\",\"InstallESD.dmg\")\n                    if \"id\" in pii: pii[\"id\"] = pii[\"id\"].replace(\"com.apple.pkg.InstallESDDmg\",\"com.apple.dmg.InstallESD\")\n                    pii.pop(\"chunklistURL\",None)\n                    pii.pop(\"chunklistid\",None)\n                with open(os.path.join(shared_support,\"InstallInfo.plist\"),\"wb\") as f:\n                    plist.dump(p,f)\n                print(\"\")\n                print(\"Created:  {}\".format(install_app))\n                print(\"Saved to: {}\".format(os.path.join(f_path,install_app)))\n                print(\"\")\n                self.u.grab(\"Press [enter] to return...\")\n            except Exception as e:\n                print(\"An error occurred:\")\n                print(\" - {}\".format(e))\n                print(\"\")\n                if len(base_mounts):\n                    for x in base_mounts:\n                        print(\" - Unmounting {}...\".format(x))\n                        self.unmount_dmg(x)\n                    print(\"\")\n                self.u.grab(\"Press [enter] to return...\")\n\nif __name__ == '__main__':\n    b = buildMacOSInstallApp()\n    b.main()\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 CorpNewt\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "MakeInstall.bat",
    "content": "@echo off\r\nREM Get our local path and args before delayed expansion - allows % and !\r\nset \"thisDir=%~dp0\"\r\nset \"args=%*\"\r\n\r\nsetlocal enableDelayedExpansion\r\nREM Setup initial vars\r\nset \"script_name=\"\r\nset /a tried=0\r\nset \"toask=yes\"\r\nset \"pause_on_error=yes\"\r\nset \"py2v=\"\r\nset \"py2path=\"\r\nset \"py3v=\"\r\nset \"py3path=\"\r\nset \"pypath=\"\r\nset \"targetpy=3\"\r\n\r\nREM use_py3:\r\nREM   TRUE  = Use if found, use py2 otherwise\r\nREM   FALSE = Use py2\r\nREM   FORCE = Use py3\r\nset \"use_py3=TRUE\"\r\n\r\nREM We'll parse if the first argument passed is\r\nREM --install-python and if so, we'll just install\r\nREM Can optionally take a version number as the\r\nREM second arg - i.e. --install-python 3.13.1\r\nset \"just_installing=FALSE\"\r\nset \"user_provided=\"\r\n\r\nREM Get the system32 (or equivalent) path\r\ncall :getsyspath \"syspath\"\r\n\r\nREM Make sure the syspath exists\r\nif \"!syspath!\" == \"\" (\r\n    if exist \"%SYSTEMROOT%\\system32\\cmd.exe\" (\r\n        if exist \"%SYSTEMROOT%\\system32\\reg.exe\" (\r\n            if exist \"%SYSTEMROOT%\\system32\\where.exe\" (\r\n                REM Fall back on the default path if it exists\r\n                set \"ComSpec=%SYSTEMROOT%\\system32\\cmd.exe\"\r\n                set \"syspath=%SYSTEMROOT%\\system32\\\"\r\n            )\r\n        )\r\n    )\r\n    if \"!syspath!\" == \"\" (\r\n        cls\r\n        echo   ###                      ###\r\n        echo  #  Missing Required Files  #\r\n        echo ###                      ###\r\n        echo.\r\n        echo Could not locate cmd.exe, reg.exe, or where.exe\r\n        echo.\r\n        echo Please ensure your ComSpec environment variable is properly configured and\r\n        echo points directly to cmd.exe, then try again.\r\n        echo.\r\n        echo Current CompSpec Value: \"%ComSpec%\"\r\n        echo.\r\n        echo Press [enter] to quit.\r\n        pause > nul\r\n        exit /b 1\r\n    )\r\n)\r\n\r\nif \"%~1\" == \"--install-python\" (\r\n    set \"just_installing=TRUE\"\r\n    set \"user_provided=%~2\"\r\n    goto installpy\r\n)\r\n\r\ngoto checkscript\r\n\r\n:checkscript\r\nREM Check for our script first\r\nset \"looking_for=!script_name!\"\r\nif \"!script_name!\" == \"\" (\r\n    set \"looking_for=%~n0.py or %~n0.command\"\r\n    set \"script_name=%~n0.py\"\r\n    if not exist \"!thisDir!\\!script_name!\" (\r\n        set \"script_name=%~n0.command\"\r\n    )\r\n)\r\nif not exist \"!thisDir!\\!script_name!\" (\r\n    cls\r\n    echo   ###                      ###\r\n    echo  #     Target Not Found     #\r\n    echo ###                      ###\r\n    echo.\r\n    echo Could not find !looking_for!.\r\n    echo Please make sure to run this script from the same directory\r\n    echo as !looking_for!.\r\n    echo.\r\n    echo Press [enter] to quit.\r\n    pause > nul\r\n    exit /b 1\r\n)\r\ngoto checkpy\r\n\r\n:checkpy\r\ncall :updatepath\r\nfor /f \"USEBACKQ tokens=*\" %%x in (`!syspath!where.exe python 2^> nul`) do ( call :checkpyversion \"%%x\" \"py2v\" \"py2path\" \"py3v\" \"py3path\" )\r\nfor /f \"USEBACKQ tokens=*\" %%x in (`!syspath!where.exe python3 2^> nul`) do ( call :checkpyversion \"%%x\" \"py2v\" \"py2path\" \"py3v\" \"py3path\" )\r\nfor /f \"USEBACKQ tokens=*\" %%x in (`!syspath!where.exe py 2^> nul`) do ( call :checkpylauncher \"%%x\" \"py2v\" \"py2path\" \"py3v\" \"py3path\" )\r\nREM Walk our returns to see if we need to install\r\nif /i \"!use_py3!\" == \"FALSE\" (\r\n    set \"targetpy=2\"\r\n    set \"pypath=!py2path!\"\r\n) else if /i \"!use_py3!\" == \"FORCE\" (\r\n    set \"pypath=!py3path!\"\r\n) else if /i \"!use_py3!\" == \"TRUE\" (\r\n    set \"pypath=!py3path!\"\r\n    if \"!pypath!\" == \"\" set \"pypath=!py2path!\"\r\n)\r\nif not \"!pypath!\" == \"\" (\r\n    goto runscript\r\n)\r\nif !tried! lss 1 (\r\n    if /i \"!toask!\"==\"yes\" (\r\n        REM Better ask permission first\r\n        goto askinstall\r\n    ) else (\r\n        goto installpy\r\n    )\r\n) else (\r\n    cls\r\n    echo   ###                      ###\r\n    echo  #     Python Not Found     #\r\n    echo ###                      ###\r\n    echo.\r\n    REM Couldn't install for whatever reason - give the error message\r\n    echo Python is not installed or not found in your PATH var.\r\n    echo Please go to https://www.python.org/downloads/windows/ to\r\n    echo download and install the latest version, then try again.\r\n    echo.\r\n    echo Make sure you check the box labeled:\r\n    echo.\r\n    echo \"Add Python X.X to PATH\"\r\n    echo.\r\n    echo Where X.X is the py version you're installing.\r\n    echo.\r\n    echo Press [enter] to quit.\r\n    pause > nul\r\n    exit /b 1\r\n)\r\ngoto runscript\r\n\r\n:checkpylauncher <path> <py2v> <py2path> <py3v> <py3path>\r\nREM Attempt to check the latest python 2 and 3 versions via the py launcher\r\nfor /f \"USEBACKQ tokens=*\" %%x in (`%~1 -2 -c \"import sys; print(sys.executable)\" 2^> nul`) do ( call :checkpyversion \"%%x\" \"%~2\" \"%~3\" \"%~4\" \"%~5\" )\r\nfor /f \"USEBACKQ tokens=*\" %%x in (`%~1 -3 -c \"import sys; print(sys.executable)\" 2^> nul`) do ( call :checkpyversion \"%%x\" \"%~2\" \"%~3\" \"%~4\" \"%~5\" )\r\ngoto :EOF\r\n\r\n:checkpyversion <path> <py2v> <py2path> <py3v> <py3path>\r\nset \"version=\"&for /f \"tokens=2* USEBACKQ delims= \" %%a in (`\"%~1\" -V 2^>^&1`) do (\r\n    REM Ensure we have a version number\r\n    call :isnumber \"%%a\"\r\n    if not \"!errorlevel!\" == \"0\" goto :EOF\r\n    set \"version=%%a\"\r\n)\r\nif not defined version goto :EOF\r\nif \"!version:~0,1!\" == \"2\" (\r\n    REM Python 2\r\n    call :comparepyversion \"!version!\" \"!%~2!\"\r\n    if \"!errorlevel!\" == \"1\" (\r\n        set \"%~2=!version!\"\r\n        set \"%~3=%~1\"\r\n    )\r\n) else (\r\n    REM Python 3\r\n    call :comparepyversion \"!version!\" \"!%~4!\"\r\n    if \"!errorlevel!\" == \"1\" (\r\n        set \"%~4=!version!\"\r\n        set \"%~5=%~1\"\r\n    )\r\n)\r\ngoto :EOF\r\n\r\n:isnumber <check_value>\r\nset \"var=\"&for /f \"delims=0123456789.\" %%i in (\"%~1\") do set var=%%i\r\nif defined var (exit /b 1)\r\nexit /b 0\r\n\r\n:comparepyversion <version1> <version2> <return>\r\nREM Exits with status 0 if equal, 1 if v1 gtr v2, 2 if v1 lss v2\r\nfor /f \"tokens=1,2,3 delims=.\" %%a in (\"%~1\") do (\r\n    set a1=%%a\r\n    set a2=%%b\r\n    set a3=%%c\r\n)\r\nfor /f \"tokens=1,2,3 delims=.\" %%a in (\"%~2\") do (\r\n    set b1=%%a\r\n    set b2=%%b\r\n    set b3=%%c\r\n)\r\nif not defined a1 set a1=0\r\nif not defined a2 set a2=0\r\nif not defined a3 set a3=0\r\nif not defined b1 set b1=0\r\nif not defined b2 set b2=0\r\nif not defined b3 set b3=0\r\nif %a1% gtr %b1% exit /b 1\r\nif %a1% lss %b1% exit /b 2\r\nif %a2% gtr %b2% exit /b 1\r\nif %a2% lss %b2% exit /b 2\r\nif %a3% gtr %b3% exit /b 1\r\nif %a3% lss %b3% exit /b 2\r\nexit /b 0\r\n\r\n:askinstall\r\ncls\r\necho   ###                      ###\r\necho  #     Python Not Found     #\r\necho ###                      ###\r\necho.\r\necho Python !targetpy! was not found on the system or in the PATH var.\r\necho.\r\nset /p \"menu=Would you like to install it now? [y/n]: \"\r\nif /i \"!menu!\"==\"y\" (\r\n    REM We got the OK - install it\r\n    goto installpy\r\n) else if \"!menu!\"==\"n\" (\r\n    REM No OK here...\r\n    set /a tried=!tried!+1\r\n    goto checkpy\r\n)\r\nREM Incorrect answer - go back\r\ngoto askinstall\r\n\r\n:installpy\r\nREM This will attempt to download and install python\r\nset /a tried=!tried!+1\r\ncls\r\necho   ###                        ###\r\necho  #     Downloading Python     #\r\necho ###                        ###\r\necho.\r\nset \"release=!user_provided!\"\r\nif \"!release!\" == \"\" (\r\n    REM No explicit release set - get the latest from python.org\r\n    echo Gathering latest version...\r\n    powershell -command \"[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12;(new-object System.Net.WebClient).DownloadFile('https://www.python.org/downloads/windows/','%TEMP%\\pyurl.txt')\"\r\n    REM Extract it if it's gzip compressed\r\n    powershell -command \"$infile='%TEMP%\\pyurl.txt';$outfile='%TEMP%\\pyurl.temp';try{$input=New-Object System.IO.FileStream $infile,([IO.FileMode]::Open),([IO.FileAccess]::Read),([IO.FileShare]::Read);$output=New-Object System.IO.FileStream $outfile,([IO.FileMode]::Create),([IO.FileAccess]::Write),([IO.FileShare]::None);$gzipStream=New-Object System.IO.Compression.GzipStream $input,([IO.Compression.CompressionMode]::Decompress);$buffer=New-Object byte[](1024);while($true){$read=$gzipstream.Read($buffer,0,1024);if($read -le 0){break};$output.Write($buffer,0,$read)};$gzipStream.Close();$output.Close();$input.Close();Move-Item -Path $outfile -Destination $infile -Force}catch{}\"\r\n    if not exist \"%TEMP%\\pyurl.txt\" (\r\n        if /i \"!just_installing!\" == \"TRUE\" (\r\n            echo  - Failed to get info\r\n            exit /b 1\r\n        ) else (\r\n            goto checkpy\r\n        )\r\n    )\r\n    pushd \"%TEMP%\"\r\n    :: Version detection code slimmed by LussacZheng (https://github.com/corpnewt/gibMacOS/issues/20)\r\n    for /f \"tokens=9 delims=< \" %%x in ('findstr /i /c:\"Latest Python !targetpy! Release\" pyurl.txt') do ( set \"release=%%x\" )\r\n    popd\r\n    REM Let's delete our txt file now - we no longer need it\r\n    del \"%TEMP%\\pyurl.txt\"\r\n    if \"!release!\" == \"\" (\r\n        if /i \"!just_installing!\" == \"TRUE\" (\r\n            echo  - Failed to get python version\r\n            exit /b 1\r\n        ) else (\r\n            goto checkpy\r\n        )\r\n    )\r\n    echo Located Version:  !release!\r\n) else (\r\n    echo User-Provided Version:  !release!\r\n    REM Update our targetpy to reflect the first number of\r\n    REM our release\r\n    for /f \"tokens=1 delims=.\" %%a in (\"!release!\") do (\r\n        call :isnumber \"%%a\"\r\n        if \"!errorlevel!\" == \"0\" (\r\n            set \"targetpy=%%a\"\r\n        )\r\n    )\r\n)\r\necho Building download url...\r\nREM At this point - we should have the version number.\r\nREM We can build the url like so: \"https://www.python.org/ftp/python/[version]/python-[version]-amd64.exe\"\r\nset \"url=https://www.python.org/ftp/python/!release!/python-!release!-amd64.exe\"\r\nset \"pytype=exe\"\r\nif \"!targetpy!\" == \"2\" (\r\n    set \"url=https://www.python.org/ftp/python/!release!/python-!release!.amd64.msi\"\r\n    set \"pytype=msi\"\r\n)\r\necho  - !url!\r\necho Downloading...\r\nREM Now we download it with our slick powershell command\r\npowershell -command \"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (new-object System.Net.WebClient).DownloadFile('!url!','%TEMP%\\pyinstall.!pytype!')\"\r\nREM If it doesn't exist - we bail\r\nif not exist \"%TEMP%\\pyinstall.!pytype!\" (\r\n    if /i \"!just_installing!\" == \"TRUE\" (\r\n        echo  - Failed to download python installer\r\n        exit /b 1\r\n    ) else (\r\n        goto checkpy\r\n    )\r\n)\r\nREM It should exist at this point - let's run it to install silently\r\necho Running python !pytype! installer...\r\npushd \"%TEMP%\"\r\nif /i \"!pytype!\" == \"exe\" (\r\n    echo  - pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0\r\n    pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0\r\n) else (\r\n    set \"foldername=!release:.=!\"\r\n    echo  - msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR=\"%LocalAppData%\\Programs\\Python\\Python!foldername:~0,2!\"\r\n    msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR=\"%LocalAppData%\\Programs\\Python\\Python!foldername:~0,2!\"\r\n)\r\npopd\r\nset \"py_error=!errorlevel!\"\r\necho Installer finished with status: !py_error!\r\necho Cleaning up...\r\nREM Now we should be able to delete the installer and check for py again\r\ndel \"%TEMP%\\pyinstall.!pytype!\"\r\nREM If it worked, then we should have python in our PATH\r\nREM this does not get updated right away though - let's try\r\nREM manually updating the local PATH var\r\ncall :updatepath\r\nif /i \"!just_installing!\" == \"TRUE\" (\r\n    echo.\r\n    echo Done.\r\n) else (\r\n    goto checkpy\r\n)\r\nexit /b\r\n\r\n:runscript\r\nREM Python found\r\ncls\r\nREM Checks the args gathered at the beginning of the script.\r\nREM Make sure we're not just forwarding empty quotes.\r\nset \"arg_test=!args:\"=!\"\r\nif \"!arg_test!\"==\"\" (\r\n    \"!pypath!\" \"!thisDir!!script_name!\"\r\n) else (\r\n    \"!pypath!\" \"!thisDir!!script_name!\" !args!\r\n)\r\nif /i \"!pause_on_error!\" == \"yes\" (\r\n    if not \"%ERRORLEVEL%\" == \"0\" (\r\n        echo.\r\n        echo Script exited with error code: %ERRORLEVEL%\r\n        echo.\r\n        echo Press [enter] to exit...\r\n        pause > nul\r\n    )\r\n)\r\ngoto :EOF\r\n\r\n:undouble <string_name> <string_value> <character>\r\nREM Helper function to strip doubles of a single character out of a string recursively\r\nset \"string_value=%~2\"\r\n:undouble_continue\r\nset \"check=!string_value:%~3%~3=%~3!\"\r\nif not \"!check!\" == \"!string_value!\" (\r\n    set \"string_value=!check!\"\r\n    goto :undouble_continue\r\n)\r\nset \"%~1=!check!\"\r\ngoto :EOF\r\n\r\n:updatepath\r\nset \"spath=\"\r\nset \"upath=\"\r\nfor /f \"USEBACKQ tokens=2* delims= \" %%i in (`!syspath!reg.exe query \"HKCU\\Environment\" /v \"Path\" 2^> nul`) do ( if not \"%%j\" == \"\" set \"upath=%%j\" )\r\nfor /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\" )\r\nif not \"%spath%\" == \"\" (\r\n    REM We got something in the system path\r\n    set \"PATH=%spath%\"\r\n    if not \"%upath%\" == \"\" (\r\n        REM We also have something in the user path\r\n        set \"PATH=%PATH%;%upath%\"\r\n    )\r\n) else if not \"%upath%\" == \"\" (\r\n    set \"PATH=%upath%\"\r\n)\r\nREM Remove double semicolons from the adjusted PATH\r\ncall :undouble \"PATH\" \"%PATH%\" \";\"\r\ngoto :EOF\r\n\r\n:getsyspath <variable_name>\r\nREM Helper method to return a valid path to cmd.exe, reg.exe, and where.exe by\r\nREM walking the ComSpec var - will also repair it in memory if need be\r\nREM Strip double semi-colons\r\ncall :undouble \"temppath\" \"%ComSpec%\" \";\"\r\n\r\nREM Dirty hack to leverage the \"line feed\" approach - there are some odd side\r\nREM effects with this.  Do not use this variable name in comments near this\r\nREM line - as it seems to behave erradically.\r\n(set LF=^\r\n%=this line is empty=%\r\n)\r\nREM Replace instances of semi-colons with a line feed and wrap\r\nREM in parenthesis to work around some strange batch behavior\r\nset \"testpath=%temppath:;=!LF!%\"\r\n\r\nREM Let's walk each path and test if cmd.exe, reg.exe, and where.exe exist there\r\nset /a found=0\r\nfor /f \"tokens=* delims=\" %%i in (\"!testpath!\") do (\r\n    REM Only continue if we haven't found it yet\r\n    if not \"%%i\" == \"\" (\r\n        if !found! lss 1 (\r\n            set \"checkpath=%%i\"\r\n            REM Remove \"cmd.exe\" from the end if it exists\r\n            if /i \"!checkpath:~-7!\" == \"cmd.exe\" (\r\n                set \"checkpath=!checkpath:~0,-7!\"\r\n            )\r\n            REM Pad the end with a backslash if needed\r\n            if not \"!checkpath:~-1!\" == \"\\\" (\r\n                set \"checkpath=!checkpath!\\\"\r\n            )\r\n            REM Let's see if cmd, reg, and where exist there - and set it if so\r\n            if EXIST \"!checkpath!cmd.exe\" (\r\n                if EXIST \"!checkpath!reg.exe\" (\r\n                    if EXIST \"!checkpath!where.exe\" (\r\n                        set /a found=1\r\n                        set \"ComSpec=!checkpath!cmd.exe\"\r\n                        set \"%~1=!checkpath!\"\r\n                    )\r\n                )\r\n            )\r\n        )\r\n    )\r\n)\r\ngoto :EOF\r\n"
  },
  {
    "path": "MakeInstall.py",
    "content": "from Scripts import utils, diskwin, downloader, run\r\nimport os, sys, tempfile, shutil, zipfile, platform, json, time\r\n\r\nclass WinUSB:\r\n\r\n    def __init__(self):\r\n        self.u = utils.Utils(\"MakeInstall\")\r\n        if not self.u.check_admin():\r\n            # Try to self-elevate\r\n            self.u.elevate(os.path.realpath(__file__))\r\n            exit()\r\n        self.min_plat = 9600\r\n        # Make sure we're on windows\r\n        self.verify_os()\r\n        # Setup initial vars\r\n        self.d = diskwin.Disk()\r\n        self.dl = downloader.Downloader()\r\n        self.r = run.Run()\r\n        self.scripts = \"Scripts\"\r\n        self.s_path  = os.path.join(os.path.dirname(os.path.realpath(__file__)), self.scripts)\r\n        # self.dd_url  = \"http://www.chrysocome.net/downloads/ddrelease64.exe\"\r\n        self.dd_url  = \"https://github.com/corpnewt/gibMacOS/files/4573241/ddrelease64.exe.zip\" # Rehost due to download issues\r\n        self.dd_name = \".\".join(os.path.basename(self.dd_url).split(\".\")[:-1]) # Get the name without the last extension\r\n        self.z_json = \"https://sourceforge.net/projects/sevenzip/best_release.json\"\r\n        self.z_url2 = \"https://www.7-zip.org/a/7z1806-x64.msi\"\r\n        self.z_url  = \"https://www.7-zip.org/a/7z[[vers]]-x64.msi\"\r\n        self.z_name = \"7z.exe\"\r\n        self.bi_url = \"https://raw.githubusercontent.com/corpnewt/gibMacOS/master/Scripts/BOOTICEx64.exe\"\r\n        self.bi_name = \"BOOTICEx64.exe\"\r\n        self.clover_url = \"https://api.github.com/repos/CloverHackyColor/CloverBootloader/releases\"\r\n        self.dids_url = \"https://api.github.com/repos/dids/clover-builder/releases\"\r\n        self.oc_url = \"https://api.github.com/repos/acidanthera/OpenCorePkg/releases\"\r\n        self.oc_boot = \"boot\"\r\n        self.oc_boot_alt = \"bootX64\"\r\n        self.oc_boot0 = \"boot0\"\r\n        self.oc_boot1 = \"boot1f32\"\r\n        # self.oc_boot_url = \"https://github.com/acidanthera/OpenCorePkg/raw/master/Utilities/LegacyBoot/\"\r\n        self.oc_boot_url = \"https://github.com/acidanthera/OpenCorePkg/raw/870017d0e5d53abeaf0347997da912c3e382a04a/Utilities/LegacyBoot/\"\r\n        self.diskpart = os.path.join(os.environ['SYSTEMDRIVE'] + \"\\\\\", \"Windows\", \"System32\", \"diskpart.exe\")\r\n        # From Tim Sutton's brigadier:  https://github.com/timsutton/brigadier/blob/master/brigadier\r\n        self.z_path = None\r\n        self.z_path64 = os.path.join(os.environ['SYSTEMDRIVE'] + \"\\\\\", \"Program Files\", \"7-Zip\", \"7z.exe\")\r\n        self.z_path32 = os.path.join(os.environ['SYSTEMDRIVE'] + \"\\\\\", \"Program Files (x86)\", \"7-Zip\", \"7z.exe\")\r\n        self.recovery_suffixes = (\r\n            \"recoveryhdupdate.pkg\",\r\n            \"recoveryhdmetadmg.pkg\",\r\n            \"basesystem.dmg\",\r\n            \"recoveryimage.dmg\"\r\n        )\r\n        self.dd_bootsector = True\r\n        self.boot0 = \"boot0af\"\r\n        self.boot1 = \"boot1f32alt\"\r\n        self.boot  = \"boot6\"\r\n        self.efi_id = \"c12a7328-f81f-11d2-ba4b-00a0c93ec93b\" # EFI\r\n        self.bas_id = \"ebd0a0a2-b9e5-4433-87c0-68b6b72699c7\" # Microsoft Basic Data\r\n        self.hfs_id = \"48465300-0000-11AA-AA11-00306543ECAC\" # HFS+\r\n        self.rec_id = \"426F6F74-0000-11AA-AA11-00306543ECAC\" # Apple Boot partition (Recovery HD)\r\n        self.show_all_disks = False\r\n    \r\n    def verify_os(self):\r\n        self.u.head(\"Verifying OS\")\r\n        print(\"\")\r\n        print(\"Verifying OS name...\")\r\n        if not os.name==\"nt\":\r\n            print(\"\")\r\n            print(\"This script is only for Windows!\")\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to exit...\")\r\n            exit(1)\r\n        print(\" - Name = NT\")\r\n        print(\"Verifying OS version...\")\r\n        # Verify we're at version 9600 or greater\r\n        try:\r\n            # Set plat to the last item of the output split by . - looks like:\r\n            # Windows-8.1-6.3.9600\r\n            # or this:\r\n            # Windows-10-10.0.17134-SP0\r\n            plat = int(platform.platform().split(\".\")[-1].split(\"-\")[0])\r\n        except:\r\n            plat = 0\r\n        if plat < self.min_plat:\r\n            print(\"\")\r\n            print(\"Currently running {}, this script requires version {} or newer.\".format(platform.platform(), self.min_plat))\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to exit...\")\r\n            exit(1)\r\n        print(\" - Version = {}\".format(plat))\r\n        print(\"\")\r\n        print(\"{} >= {}, continuing...\".format(plat, self.min_plat))\r\n\r\n    def get_disks_of_type(self, disk_list, disk_type=(0,2)):\r\n        disks = {}\r\n        for disk in disk_list:\r\n            if disk_list[disk].get(\"type\",0) in disk_type:\r\n                disks[disk] = disk_list[disk]\r\n        return disks\r\n\r\n    def check_dd(self):\r\n        # Checks if ddrelease64.exe exists in our Scripts dir\r\n        # and if not - downloads it\r\n        #\r\n        # Returns True if exists/downloaded successfully\r\n        # or False if issues.\r\n        # Check for dd.exe in the current dir\r\n        if os.path.exists(os.path.join(self.s_path, self.dd_name)):\r\n            # print(\"Located {}!\".format(self.dd_name))\r\n            # Got it\r\n            return True\r\n        print(\"Couldn't locate {} - downloading...\".format(self.dd_name))\r\n        temp = tempfile.mkdtemp()\r\n        z_file = os.path.basename(self.dd_url)\r\n        # Now we need to download\r\n        self.dl.stream_to_file(self.dd_url, os.path.join(temp,z_file))\r\n        print(\" - Extracting...\")\r\n        # Extract with built-in tools \\o/\r\n        cwd = os.getcwd()\r\n        os.chdir(temp)\r\n        with zipfile.ZipFile(os.path.join(temp,z_file)) as z:\r\n            z.extractall(temp)\r\n        for x in os.listdir(temp):\r\n            if self.dd_name.lower() == x.lower():\r\n                # Found it\r\n                print(\" - Found {}\".format(x))\r\n                print(\"   - Copying to {} directory...\".format(self.scripts))\r\n                shutil.copy(os.path.join(temp,x), os.path.join(self.s_path,x))\r\n        # Return to prior cwd\r\n        os.chdir(cwd)\r\n        # Remove the temp folder\r\n        shutil.rmtree(temp,ignore_errors=True)\r\n        print(\"\")\r\n        return os.path.exists(os.path.join(self.s_path, self.dd_name))\r\n\r\n    def check_7z(self):\r\n        # Check the PATH var first\r\n        z_path = self.r.run({\"args\":[\"where.exe\",\"7z.exe\"]})[0].split(\"\\n\")[0].rstrip(\"\\r\")\r\n        self.z_path = next((x for x in (z_path,self.z_path64,self.z_path32) if x and os.path.isfile(x)),None)\r\n        if self.z_path:\r\n            return True\r\n        print(\"Didn't locate {} - downloading...\".format(self.z_name))\r\n        # Didn't find it - let's do some stupid stuff\r\n        # First we get our json response - or rather, try to, then parse it\r\n        # looking for the current version\r\n        dl_url = None\r\n        try:\r\n            json_data = json.loads(self.dl.get_string(self.z_json))\r\n            v_num = json_data.get(\"release\",{}).get(\"filename\",\"\").split(\"/\")[-1].lower().split(\"-\")[0].replace(\"7z\",\"\").replace(\".exe\",\"\")\r\n            if len(v_num):\r\n                dl_url = self.z_url.replace(\"[[vers]]\",v_num)\r\n        except:\r\n            pass\r\n        if not dl_url:\r\n            dl_url = self.z_url2\r\n        temp = tempfile.mkdtemp()\r\n        dl_file = self.dl.stream_to_file(dl_url, os.path.join(temp, self.z_name))\r\n        if not dl_file: # Didn't download right\r\n            shutil.rmtree(temp,ignore_errors=True)\r\n            return False\r\n        print(\"\")\r\n        print(\"Installing 7zip...\")\r\n        # From Tim Sutton's brigadier:  https://github.com/timsutton/brigadier/blob/master/brigadier\r\n        out = self.r.run({\"args\":[\"msiexec\", \"/qn\", \"/i\", os.path.join(temp, self.z_name)],\"stream\":True})\r\n        if out[2] != 0:\r\n            shutil.rmtree(temp,ignore_errors=True)\r\n            print(\"Error ({})\".format(out[2]))\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to exit...\")\r\n            exit(1)\r\n        print(\"\")\r\n        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\r\n        return self.z_path and os.path.exists(self.z_path)\r\n\r\n    def check_bi(self):\r\n        # Checks for BOOTICEx64.exe in our scripts dir\r\n        # and downloads it if need be\r\n        if os.path.exists(os.path.join(self.s_path, self.bi_name)):\r\n            # print(\"Located {}!\".format(self.bi_name))\r\n            # Got it\r\n            return True\r\n        print(\"Couldn't locate {} - downloading...\".format(self.bi_name))\r\n        self.dl.stream_to_file(self.bi_url, os.path.join(self.s_path, self.bi_name))\r\n        print(\"\")\r\n        return os.path.exists(os.path.join(self.s_path,self.bi_name))\r\n\r\n    def get_dl_url_from_json(self,json_data,suffix=(\".lzma\",\".iso.7z\")):\r\n        try: j_list = json.loads(json_data)\r\n        except: return None\r\n        j_list = j_list if isinstance(j_list,list) else [j_list]\r\n        for j in j_list:\r\n            dl_link = next((x.get(\"browser_download_url\", None) for x in j.get(\"assets\", []) if x.get(\"browser_download_url\", \"\").endswith(suffix)), None)\r\n            if dl_link: break\r\n        if not dl_link:\r\n            return None\r\n        return { \"url\" : dl_link, \"name\" : os.path.basename(dl_link), \"info\" : j.get(\"body\", None) }\r\n\r\n    def get_dl_info(self,clover_version=None):\r\n        # Returns the latest download package and info in a\r\n        # dictionary:  { \"url\" : dl_url, \"name\" : name, \"info\" : update_info }\r\n        # Attempt Dids' repo first - falling back on Clover's official repo as needed\r\n        clover_urls = (self.clover_url,self.dids_url)\r\n        try:\r\n            assert int(clover_version) <= 5122 # Check if we're trying to get r5122 or prior\r\n            # If we didn't throw an exception, we can reverse the order of the URLs to check\r\n            # Dids' builder first\r\n            clover_urls = (self.dids_url,self.clover_url)\r\n        except:\r\n            pass # Wasn't a proper int, or was above 5122\r\n        for url in clover_urls:\r\n            # Tag is e.g. 5098 on Slice's repo, and e.g. v2.5k_r5098 or v5.0_r5xxx on Dids'\r\n            # accommodate as needed\r\n            if not clover_version:\r\n                search_url = url\r\n            elif url == self.clover_url:\r\n                # Using CloverHackyColor's repo - set the tag to the version\r\n                search_url = \"{}/tags/{}\".format(url,clover_version)\r\n            else:\r\n                # Using Dids' clover builder - figure out the prefix based on the version\r\n                search_url = \"{}/tags/v{}_r{}\".format(url,\"5.0\" if clover_version >= \"5118\" else \"2.5k\",clover_version)\r\n            print(\" - Checking {}\".format(search_url))\r\n            json_data = self.dl.get_string(search_url, False)\r\n            if not json_data: print(\" --> Not found!\")\r\n            else: return self.get_dl_url_from_json(json_data)\r\n        return None\r\n\r\n    def get_oc_dl_info(self):\r\n        json_data = self.dl.get_string(self.oc_url, False)\r\n        if not json_data: print(\" --> Not found!\")\r\n        else: return self.get_dl_url_from_json(json_data,suffix=\"-RELEASE.zip\")\r\n\r\n    def diskpart_flag(self, disk, as_efi=False):\r\n        # Sets and unsets the GUID needed for a GPT EFI partition ID\r\n        self.u.head(\"Changing ID With DiskPart\")\r\n        print(\"\")\r\n        print(\"Setting type as {}...\".format(\"EFI\" if as_efi else \"Basic Data\"))\r\n        print(\"\")\r\n        # - EFI system partition: c12a7328-f81f-11d2-ba4b-00a0c93ec93b\r\n        # - Basic data partition: ebd0a0a2-b9e5-4433-87c0-68b6b72699c7\r\n        dp_script = \"\\n\".join([\r\n            \"select disk {}\".format(disk.get(\"index\",-1)),\r\n            \"sel part 1\",\r\n            \"set id={}\".format(self.efi_id if as_efi else self.bas_id)\r\n        ])\r\n        temp = tempfile.mkdtemp()\r\n        script = os.path.join(temp, \"diskpart.txt\")\r\n        try:\r\n            with open(script,\"w\") as f:\r\n                f.write(dp_script)\r\n        except:\r\n            shutil.rmtree(temp)\r\n            print(\"Error creating script!\")\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return...\")\r\n            return\r\n        # Let's try to run it!\r\n        out = self.r.run({\"args\":[self.diskpart,\"/s\",script],\"stream\":True})\r\n        # Ditch our script regardless of whether diskpart worked or not\r\n        shutil.rmtree(temp)\r\n        print(\"\")\r\n        if out[2] != 0:\r\n            # Error city!\r\n            print(\"DiskPart exited with non-zero status ({}).  Aborting.\".format(out[2]))\r\n        else:\r\n            print(\"Done - You may need to replug your drive for the\")\r\n            print(\"changes to take effect.\")\r\n        print(\"\")\r\n        self.u.grab(\"Press [enter] to return...\")\r\n\r\n    def diskpart_erase(self, disk, gpt=False, clover_version = None, local_file = None):\r\n        # Generate a script that we can pipe to diskpart to erase our disk\r\n        self.u.head(\"Erasing With DiskPart\")\r\n        print(\"\")\r\n        # Then we'll re-gather our disk info on success and move forward\r\n        # Using MBR to effectively set the individual partition types\r\n        # Keeps us from having issues mounting the EFI on Windows -\r\n        # and also lets us explicitly set the partition id for the main\r\n        # data partition.\r\n        if not gpt:\r\n            print(\"Using MBR...\")\r\n            dp_script = \"\\n\".join([\r\n                \"select disk {}\".format(disk.get(\"index\",-1)),\r\n                \"clean\",\r\n                \"convert mbr\",\r\n                \"create partition primary size=200\",\r\n                \"format quick fs=fat32 label='BOOT'\",\r\n                \"active\",\r\n                \"create partition primary\",\r\n                \"select part 2\",\r\n                \"set id=AB\", # AF = HFS, AB = Recovery\r\n                \"select part 1\",\r\n                \"assign\"\r\n            ])\r\n        else:\r\n            print(\"Using GPT...\")\r\n            dp_script = \"\\n\".join([\r\n                \"select disk {}\".format(disk.get(\"index\",-1)),\r\n                \"clean\",\r\n                \"convert gpt\",\r\n                \"create partition primary size=200\",\r\n                \"format quick fs=fat32 label='BOOT'\",\r\n                \"create partition primary id={}\".format(self.hfs_id)\r\n            ])\r\n        temp = tempfile.mkdtemp()\r\n        script = os.path.join(temp, \"diskpart.txt\")\r\n        try:\r\n            with open(script,\"w\") as f:\r\n                f.write(dp_script)\r\n        except:\r\n            shutil.rmtree(temp)\r\n            print(\"Error creating script!\")\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return...\")\r\n            return\r\n        # Let's try to run it!\r\n        out = self.r.run({\"args\":[self.diskpart,\"/s\",script],\"stream\":True})\r\n        # Ditch our script regardless of whether diskpart worked or not\r\n        shutil.rmtree(temp)\r\n        if out[2] != 0:\r\n            # Error city!\r\n            print(\"\")\r\n            print(\"DiskPart exited with non-zero status ({}).  Aborting.\".format(out[2]))\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return...\")\r\n            return\r\n        # We should now have a fresh drive to work with\r\n        # Let's write an image or something\r\n        self.u.head(\"Updating Disk Information\")\r\n        print(\"\")\r\n        print(\"Re-populating list...\")\r\n        self.d.update()\r\n        print(\"Relocating disk {}\".format(disk[\"index\"]))\r\n        disk = self.d.disks[str(disk[\"index\"])]\r\n        self.select_package(disk, clover_version, local_file=local_file)\r\n\r\n    def select_package(self, disk, clover_version = None, local_file = None):\r\n        self.u.head(\"Select Recovery Package\")\r\n        print(\"\")\r\n        print(\"{}. {} - {} ({})\".format(\r\n            disk.get(\"index\",-1), \r\n            disk.get(\"model\",\"Unknown\"), \r\n            self.dl.get_size(disk.get(\"size\",-1),strip_zeroes=True),\r\n            [\"Unknown\",\"No Root Dir\",\"Removable\",\"Local\",\"Network\",\"Disc\",\"RAM Disk\"][disk.get(\"type\",0)]\r\n            ))\r\n        print(\"\")\r\n        print(\"M. Main Menu\")\r\n        print(\"Q. Quit\")\r\n        print(\"\")\r\n        print(\"(To copy a file's path, shift + right-click in Explorer and select 'Copy as path')\\n\")\r\n        menu = self.u.grab(\"Please paste the recovery update pkg/dmg path to extract:  \")\r\n        if menu.lower() == \"q\":\r\n            self.u.custom_quit()\r\n        if menu.lower() == \"m\":\r\n            return\r\n        path = self.u.check_path(menu)\r\n        if not path:\r\n            self.select_package(disk, clover_version, local_file=local_file)\r\n            return\r\n        # Got the package - let's make sure it's named right - just in case\r\n        if os.path.basename(path).lower().endswith(\".hfs\"):\r\n            # We have an hfs image already - bypass extraction\r\n            self.dd_image(disk, path, clover_version, local_file=local_file)\r\n            return\r\n        # If it's a directory, find the first recovery hit\r\n        if os.path.isdir(path):\r\n            for f in os.listdir(path):\r\n                if f.lower().endswith(self.recovery_suffixes):\r\n                    path = os.path.join(path, f)\r\n                    break\r\n        # Make sure it's named right for recovery stuffs\r\n        if not path.lower().endswith(self.recovery_suffixes):\r\n            self.u.head(\"Invalid Package\")\r\n            print(\"\")\r\n            print(\"{} is not in the available recovery package names:\\n{}\".format(os.path.basename(path), \", \".join(self.recovery_suffixes)))\r\n            print(\"\")\r\n            print(\"Ensure you're passing a proper recovery package.\")\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return to package selection...\")\r\n            self.select_package(disk, clover_version, local_file=local_file)\r\n            return\r\n        self.u.head(\"Extracting Package\")\r\n        print(\"\")\r\n        temp = tempfile.mkdtemp()\r\n        cwd = os.getcwd()\r\n        os.chdir(temp)\r\n        print(\"Located {}...\".format(os.path.basename(path)))\r\n        if not path.lower().endswith(\".dmg\"):\r\n            # Extract in sections and remove any files we run into\r\n            print(\"Extracting Recovery dmg...\")\r\n            out = self.r.run({\"args\":[self.z_path, \"e\", \"-txar\", path, \"*.dmg\"]})\r\n            if out[2] != 0:\r\n                shutil.rmtree(temp,ignore_errors=True)\r\n                print(\"An error occurred extracting: {}\".format(out[2]))\r\n                print(\"\")\r\n                self.u.grab(\"Press [enter] to return...\")\r\n                return\r\n            print(\"Extracting BaseSystem.dmg...\")\r\n            # No files to delete here - let's extract the next part\r\n            out = self.r.run({\"args\":[self.z_path, \"e\", \"*.dmg\", \"*/Base*.dmg\"]})\r\n            if out[2] != 0:\r\n                shutil.rmtree(temp,ignore_errors=True)\r\n                print(\"An error occurred extracting: {}\".format(out[2]))\r\n                print(\"\")\r\n                self.u.grab(\"Press [enter] to return...\")\r\n                return\r\n            # If we got here - we should delete everything in the temp folder except\r\n            # for a .dmg that starts with Base\r\n            del_list = [x for x in os.listdir(temp) if not (x.lower().startswith(\"base\") and x.lower().endswith(\".dmg\"))]\r\n            for d in del_list:\r\n                os.remove(os.path.join(temp, d))\r\n        # Onto the last command\r\n        print(\"Extracting hfs...\")\r\n        out = self.r.run({\"args\":[self.z_path, \"e\", \"-tdmg\", path if path.lower().endswith(\".dmg\") else \"Base*.dmg\", \"*.hfs\"]})\r\n        if out[2] != 0:\r\n            shutil.rmtree(temp,ignore_errors=True)\r\n            print(\"An error occurred extracting: {}\".format(out[2]))\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return...\")\r\n            return\r\n        # If we got here - we should delete everything in the temp folder except\r\n        # for a .dmg that starts with Base\r\n        del_list = [x for x in os.listdir(temp) if not x.lower().endswith(\".hfs\")]\r\n        for d in del_list:\r\n            os.remove(os.path.join(temp, d))\r\n        print(\"Extracted successfully!\")\r\n        hfs = next((x for x in os.listdir(temp) if x.lower().endswith(\".hfs\")),None)\r\n        # Now to dd our image - if it exists\r\n        if not hfs:\r\n            print(\"Missing the .hfs file!  Aborting.\")\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return...\")\r\n        else:\r\n            self.dd_image(disk, os.path.join(temp, hfs), clover_version, local_file=local_file)\r\n        shutil.rmtree(temp,ignore_errors=True)\r\n\r\n    def dd_image(self, disk, image, clover_version = None, local_file = None):\r\n        # Let's dd the shit out of our disk\r\n        self.u.head(\"Copying Image To Drive\")\r\n        print(\"\")\r\n        print(\"Image: {}\".format(image))\r\n        print(\"\")\r\n        print(\"Disk {}. {} - {} ({})\".format(\r\n            disk.get(\"index\",-1), \r\n            disk.get(\"model\",\"Unknown\"), \r\n            self.dl.get_size(disk.get(\"size\",-1),strip_zeroes=True),\r\n            [\"Unknown\",\"No Root Dir\",\"Removable\",\"Local\",\"Network\",\"Disc\",\"RAM Disk\"][disk.get(\"type\",0)]\r\n            ))\r\n        print(\"\")\r\n        args = [\r\n            os.path.join(self.s_path, self.dd_name),\r\n            \"if={}\".format(image),\r\n            \"of=\\\\\\\\?\\\\Device\\\\Harddisk{}\\\\Partition2\".format(disk.get(\"index\",-1)),\r\n            \"bs=8M\",\r\n            \"--progress\"\r\n        ]\r\n        print(\" \".join(args))\r\n        print(\"\")\r\n        print(\"This may take some time!\")\r\n        print(\"\")\r\n        out = self.r.run({\"args\":args})\r\n        if len(out[1].split(\"Error\")) > 1:\r\n            # We had some error text - dd, even when failing likes to give us a 0\r\n            # status code.  It also sends a ton of text through stderr - so we comb\r\n            # that for \"Error\" then split by that to skip the extra fluff and show only\r\n            # the error.\r\n            print(\"An error occurred:\\n\\n{}\".format(\"Error\"+out[1].split(\"Error\")[1]))\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return to the main menu...\")\r\n            return\r\n        # Install Clover/OC to the target drive\r\n        if clover_version == \"OpenCore\": self.install_oc(disk, local_file=local_file)\r\n        else: self.install_clover(disk, clover_version, local_file=local_file)\r\n\r\n    def install_oc(self, disk, local_file = None):\r\n        self.u.head(\"Installing OpenCore\")\r\n        print(\"\")\r\n        print(\"Gathering info...\")\r\n        if not local_file:\r\n            o = self.get_oc_dl_info()\r\n            if o is None:\r\n                print(\" - Error communicating with github!\")\r\n                print(\"\")\r\n                self.u.grab(\"Press [enter] to return...\")\r\n                return\r\n            print(\" - Got {}\".format(o.get(\"name\",\"Unknown Version\")))\r\n            print(\"Downloading...\")\r\n            temp = tempfile.mkdtemp()\r\n            os.chdir(temp)\r\n            self.dl.stream_to_file(o[\"url\"], os.path.join(temp, o[\"name\"]))\r\n        else:\r\n            print(\"Using local file: {}\".format(local_file))\r\n            temp = tempfile.mkdtemp()\r\n            os.chdir(temp)\r\n            o = {\"name\":os.path.basename(local_file)}\r\n            # Copy to the temp folder\r\n            shutil.copy(local_file,os.path.join(temp,o[\"name\"]))\r\n        print(\"\") # Empty space to clear the download progress\r\n        if not os.path.exists(os.path.join(temp, o[\"name\"])):\r\n            shutil.rmtree(temp,ignore_errors=True)\r\n            print(\" - Download failed.  Aborting...\")\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return...\")\r\n            return\r\n        oc_zip = o[\"name\"]\r\n        # Got a valid file in our temp dir\r\n        print(\"Extracting {}...\".format(oc_zip))\r\n        out = self.r.run({\"args\":[self.z_path, \"x\", os.path.join(temp,oc_zip)]})\r\n        if out[2] != 0:\r\n            shutil.rmtree(temp,ignore_errors=True)\r\n            print(\" - An error occurred extracting: {}\".format(out[2]))\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return...\")\r\n            return\r\n        # We need to also gather our boot, boot0af, and boot1f32 files\r\n        print(\"Gathering DUET boot files...\")\r\n        uefi_only = False\r\n        duet_loc = os.path.join(temp,\"Utilities\",\"LegacyBoot\")\r\n        for x in (self.oc_boot,self.oc_boot_alt,self.oc_boot0,self.oc_boot1):\r\n            # Check the local dir first\r\n            if os.path.exists(os.path.join(duet_loc,x)):\r\n                print(\" - {}\".format(x))\r\n                # Copy it over\r\n                target_name = self.oc_boot if x == self.oc_boot_alt else x\r\n                shutil.copy(os.path.join(duet_loc,x), os.path.join(temp,target_name))\r\n        missing_list = [x for x in (self.oc_boot,self.oc_boot0,self.oc_boot1) if not os.path.exists(os.path.join(temp,x))]\r\n        if missing_list:\r\n            print(\" - Missing: {}\".format(\", \".join(missing_list)))\r\n            print(\"Attempting to download...\")\r\n            for x in missing_list:\r\n                print(\" - {}\".format(x))\r\n                self.dl.stream_to_file(self.oc_boot_url + x, os.path.join(temp,x),False)\r\n            if not all((os.path.exists(os.path.join(temp,x)) for x in missing_list)):\r\n                print(\"Could not located all required DUET files - USB will be UEFI ONLY\")\r\n                uefi_only = True\r\n        # At this point, we should have a boot0xx file and an EFI folder in the temp dir\r\n        # We need to udpate the disk list though - to reflect the current file system on part 1\r\n        # of our current disk\r\n        self.d.update() # assumes our disk number stays the same\r\n        # Some users are having issues with the \"partitions\" key not populating - possibly a 3rd party disk management soft?\r\n        # Possibly a bad USB?\r\n        # We'll see if the key exists - if not, we'll throw an error.\r\n        if self.d.disks[str(disk[\"index\"])].get(\"partitions\",None) is None:\r\n            # No partitions found.\r\n            shutil.rmtree(temp,ignore_errors=True)\r\n            print(\"No partitions located on disk!\")\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return...\")\r\n            return\r\n        part = self.d.disks[str(disk[\"index\"])][\"partitions\"].get(\"0\",{}).get(\"letter\",None) # get the first partition's letter\r\n        if part is None:\r\n            shutil.rmtree(temp,ignore_errors=True)\r\n            print(\"Lost original disk - or formatting failed!\")\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return...\")\r\n            return\r\n        # Here we have our disk and partitions and such - the BOOT partition\r\n        # will be the first partition\r\n        # Let's copy over the EFI folder and then dd the boot0xx file\r\n        print(\"Copying EFI folder to {}/EFI...\".format(part))\r\n        source_efi = None\r\n        if os.path.exists(os.path.join(temp,\"EFI\")):\r\n            source_efi = os.path.join(temp,\"EFI\")\r\n        elif os.path.exists(os.path.join(temp,\"X64\",\"EFI\")):\r\n            source_efi = os.path.join(temp,\"X64\",\"EFI\")\r\n        if not source_efi:\r\n            print(\" - Source EFI not found!\")\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return...\")\r\n            return\r\n        if os.path.exists(\"{}/EFI\".format(part)):\r\n            print(\" - EFI exists - removing...\")\r\n            shutil.rmtree(\"{}/EFI\".format(part),ignore_errors=True)\r\n            time.sleep(1) # Added because windows is dumb\r\n        shutil.copytree(source_efi, \"{}/EFI\".format(part))\r\n        if not uefi_only:\r\n            # Copy boot over to the root of the EFI volume\r\n            print(\"Copying {} to {}/boot...\".format(self.oc_boot,part))\r\n            shutil.copy(os.path.join(temp,self.oc_boot),\"{}/boot\".format(part))\r\n            # Use bootice to update the MBR and PBR - always on the first\r\n            # partition (which is 0 in bootice)\r\n            print(\"Updating the MBR with {}...\".format(self.oc_boot0))\r\n            args = [\r\n                os.path.join(self.s_path,self.bi_name),\r\n                \"/device={}\".format(disk.get(\"index\",-1)),\r\n                \"/mbr\",\r\n                \"/restore\",\r\n                \"/file={}\".format(os.path.join(temp,self.oc_boot0)),\r\n                \"/keep_dpt\",\r\n                \"/quiet\"\r\n            ]\r\n            out = self.r.run({\"args\":args})\r\n            if out[2] != 0:\r\n                shutil.rmtree(temp,ignore_errors=True)\r\n                print(\" - An error occurred updating the MBR: {}\".format(out[2]))\r\n                print(\"\")\r\n                self.u.grab(\"Press [enter] to return...\")\r\n                return\r\n            print(\"Updating the PBR with {}...\".format(self.oc_boot1))\r\n            args = [\r\n                os.path.join(self.s_path,self.bi_name),\r\n                \"/device={}:0\".format(disk.get(\"index\",-1)),\r\n                \"/pbr\",\r\n                \"/restore\",\r\n                \"/file={}\".format(os.path.join(temp,self.oc_boot1)),\r\n                \"/keep_bpb\",\r\n                \"/quiet\"\r\n            ]\r\n            out = self.r.run({\"args\":args})\r\n            if out[2] != 0:\r\n                shutil.rmtree(temp,ignore_errors=True)\r\n                print(\" - An error occurred updating the PBR: {}\".format(out[2]))\r\n                print(\"\")\r\n                self.u.grab(\"Press [enter] to return...\")\r\n                return\r\n        print(\"Cleaning up...\")\r\n        shutil.rmtree(temp,ignore_errors=True)\r\n        print(\"\")\r\n        print(\"Done.\")\r\n        print(\"\")\r\n        self.u.grab(\"Press [enter] to return to the main menu...\")\r\n\r\n    def install_clover(self, disk, clover_version = None, local_file = None):\r\n        self.u.head(\"Installing Clover - {}\".format(\"Latest\" if not clover_version else \"r\"+clover_version))\r\n        print(\"\")\r\n        print(\"Gathering info...\")\r\n        if not local_file:\r\n            c = self.get_dl_info(clover_version)\r\n            if c is None:\r\n                if clover_version is None: print(\" - Error communicating with github!\")\r\n                else: print(\" - Error gathering info for Clover r{}\".format(clover_version))\r\n                print(\"\")\r\n                self.u.grab(\"Press [enter] to return...\")\r\n                return\r\n            print(\" - Got {}\".format(c.get(\"name\",\"Unknown Version\")))\r\n            print(\"Downloading...\")\r\n            temp = tempfile.mkdtemp()\r\n            os.chdir(temp)\r\n            self.dl.stream_to_file(c[\"url\"], os.path.join(temp, c[\"name\"]))\r\n        else:\r\n            print(\"Using local file: {}\".format(local_file))\r\n            temp = tempfile.mkdtemp()\r\n            os.chdir(temp)\r\n            c = {\"name\":os.path.basename(local_file)}\r\n            # Copy to the temp folder\r\n            shutil.copy(local_file,os.path.join(temp,c[\"name\"]))\r\n        print(\"\") # Empty space to clear the download progress\r\n        if not os.path.exists(os.path.join(temp, c[\"name\"])):\r\n            shutil.rmtree(temp,ignore_errors=True)\r\n            print(\" - Download failed.  Aborting...\")\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return...\")\r\n            return\r\n        clover_archive = c[\"name\"]\r\n        # Got a valid file in our temp dir\r\n        print(\"Extracting {}...\".format(clover_archive))\r\n        out = self.r.run({\"args\":[self.z_path, \"e\", os.path.join(temp,clover_archive)]})\r\n        if out[2] != 0:\r\n            shutil.rmtree(temp,ignore_errors=True)\r\n            print(\" - An error occurred extracting: {}\".format(out[2]))\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return...\")\r\n            return\r\n        # Should result in a .tar file\r\n        clover_tar = next((x for x in os.listdir(temp) if x.lower().endswith(\".tar\")),None)\r\n        if clover_tar:\r\n            # Got a .tar archive - get the .iso\r\n            print(\"Extracting {}...\".format(clover_tar))\r\n            out = self.r.run({\"args\":[self.z_path, \"e\", os.path.join(temp,clover_tar)]})\r\n            if out[2] != 0:\r\n                shutil.rmtree(temp,ignore_errors=True)\r\n                print(\" - An error occurred extracting: {}\".format(out[2]))\r\n                print(\"\")\r\n                self.u.grab(\"Press [enter] to return...\")\r\n                return\r\n        # Should result in a .iso file\r\n        clover_iso = next((x for x in os.listdir(temp) if x.lower().endswith(\".iso\")),None)\r\n        if not clover_iso:\r\n            shutil.rmtree(temp,ignore_errors=True)\r\n            print(\" - No .iso found - aborting...\")\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return...\")\r\n            return\r\n        # Got the .iso - let's extract the needed parts\r\n        print(\"Extracting EFI from {}...\".format(clover_iso))\r\n        out = self.r.run({\"args\":[self.z_path, \"x\", os.path.join(temp,clover_iso), \"EFI*\"]})\r\n        if out[2] != 0:\r\n            shutil.rmtree(temp,ignore_errors=True)\r\n            print(\" - An error occurred extracting: {}\".format(out[2]))\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return...\")\r\n            return\r\n        print(\"Extracting {} from {}...\".format(self.boot0,clover_iso))\r\n        out = self.r.run({\"args\":[self.z_path, \"e\", os.path.join(temp,clover_iso), self.boot0, \"-r\"]})\r\n        if out[2] != 0:\r\n            shutil.rmtree(temp,ignore_errors=True)\r\n            print(\" - An error occurred extracting: {}\".format(out[2]))\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return...\")\r\n            return\r\n        print(\"Extracting {} from {}...\".format(self.boot1,clover_iso))\r\n        out = self.r.run({\"args\":[self.z_path, \"e\", os.path.join(temp,clover_iso), self.boot1, \"-r\"]})\r\n        if out[2] != 0:\r\n            shutil.rmtree(temp,ignore_errors=True)\r\n            print(\" - An error occurred extracting: {}\".format(out[2]))\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return...\")\r\n            return\r\n        print(\"Extracting {} from {}...\".format(self.boot,clover_iso))\r\n        out = self.r.run({\"args\":[self.z_path, \"e\", os.path.join(temp,clover_iso), self.boot, \"-r\"]})\r\n        if out[2] != 0:\r\n            shutil.rmtree(temp,ignore_errors=True)\r\n            print(\" - An error occurred extracting: {}\".format(out[2]))\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return...\")\r\n            return\r\n        # At this point, we should have a boot0xx file and an EFI folder in the temp dir\r\n        # We need to udpate the disk list though - to reflect the current file system on part 1\r\n        # of our current disk\r\n        self.d.update() # assumes our disk number stays the same\r\n        # Some users are having issues with the \"partitions\" key not populating - possibly a 3rd party disk management soft?\r\n        # Possibly a bad USB?\r\n        # We'll see if the key exists - if not, we'll throw an error.\r\n        if self.d.disks[str(disk[\"index\"])].get(\"partitions\",None) is None:\r\n            # No partitions found.\r\n            shutil.rmtree(temp,ignore_errors=True)\r\n            print(\"No partitions located on disk!\")\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return...\")\r\n            return\r\n        part = self.d.disks[str(disk[\"index\"])][\"partitions\"].get(\"0\",{}).get(\"letter\",None) # get the first partition's letter\r\n        if part is None:\r\n            shutil.rmtree(temp,ignore_errors=True)\r\n            print(\"Lost original disk - or formatting failed!\")\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return...\")\r\n            return\r\n        # Here we have our disk and partitions and such - the CLOVER partition\r\n        # will be the first partition\r\n        # Let's copy over the EFI folder and then dd the boot0xx file\r\n        print(\"Copying EFI folder to {}/EFI...\".format(part))\r\n        if os.path.exists(\"{}/EFI\".format(part)):\r\n            print(\" - EFI exists - removing...\")\r\n            shutil.rmtree(\"{}/EFI\".format(part),ignore_errors=True)\r\n            time.sleep(1) # Added because windows is dumb\r\n        shutil.copytree(os.path.join(temp,\"EFI\"), \"{}/EFI\".format(part))\r\n        # Copy boot6 over to the root of the EFI volume - and rename it to boot\r\n        print(\"Copying {} to {}/boot...\".format(self.boot,part))\r\n        shutil.copy(os.path.join(temp,self.boot),\"{}/boot\".format(part))\r\n        # Use bootice to update the MBR and PBR - always on the first\r\n        # partition (which is 0 in bootice)\r\n        print(\"Updating the MBR with {}...\".format(self.boot0))\r\n        args = [\r\n            os.path.join(self.s_path,self.bi_name),\r\n            \"/device={}\".format(disk.get(\"index\",-1)),\r\n            \"/mbr\",\r\n            \"/restore\",\r\n            \"/file={}\".format(os.path.join(temp,self.boot0)),\r\n            \"/keep_dpt\",\r\n            \"/quiet\"\r\n        ]\r\n        out = self.r.run({\"args\":args})\r\n        if out[2] != 0:\r\n            shutil.rmtree(temp,ignore_errors=True)\r\n            print(\" - An error occurred updating the MBR: {}\".format(out[2]))\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return...\")\r\n            return\r\n        print(\"Updating the PBR with {}...\".format(self.boot1))\r\n        args = [\r\n            os.path.join(self.s_path,self.bi_name),\r\n            \"/device={}:0\".format(disk.get(\"index\",-1)),\r\n            \"/pbr\",\r\n            \"/restore\",\r\n            \"/file={}\".format(os.path.join(temp,self.boot1)),\r\n            \"/keep_bpb\",\r\n            \"/quiet\"\r\n        ]\r\n        out = self.r.run({\"args\":args})\r\n        if out[2] != 0:\r\n            shutil.rmtree(temp,ignore_errors=True)\r\n            print(\" - An error occurred updating the PBR: {}\".format(out[2]))\r\n            print(\"\")\r\n            self.u.grab(\"Press [enter] to return...\")\r\n            return\r\n        print(\"Cleaning up...\")\r\n        shutil.rmtree(temp,ignore_errors=True)\r\n        print(\"\")\r\n        print(\"Done.\")\r\n        print(\"\")\r\n        self.u.grab(\"Press [enter] to return to the main menu...\")\r\n\r\n    def main(self):\r\n        # Start out with our cd in the right spot.\r\n        os.chdir(os.path.dirname(os.path.realpath(__file__)))\r\n        # Let's make sure we have the required files needed\r\n        self.u.head(\"Checking Required Tools\")\r\n        print(\"\")\r\n        if not self.check_dd():\r\n            print(\"Couldn't find or install {} - aborting!\\n\".format(self.dd_name))\r\n            self.u.grab(\"Press [enter] to exit...\")\r\n            exit(1)\r\n        if not self.check_7z():\r\n            print(\"Couldn't find or install {} - aborting!\\n\".format(self.z_name))\r\n            self.u.grab(\"Press [enter] to exit...\")\r\n            exit(1)\r\n        if not self.check_bi():\r\n            print(\"Couldn't find or install {} - aborting!\\n\".format(self.bi_name))\r\n            self.u.grab(\"Press [enter] to exit...\")\r\n            exit(1)\r\n        # Let's just setup a real simple interface and try to write some data\r\n        self.u.head(\"Gathering Disk Info\")\r\n        print(\"\")\r\n        print(\"Populating list...\")\r\n        self.d.update()\r\n        print(\"\")\r\n        print(\"Done!\")\r\n        # Let's serve up a list of *only* removable media\r\n        self.u.head(\"Potential Removable Media\")\r\n        print(\"\")\r\n        rem_disks = self.get_disks_of_type(self.d.disks) if not self.show_all_disks else self.d.disks\r\n\r\n        # Types: 0 = Unknown, 1 = No Root Dir, 2 = Removable, 3 = Local, 4 = Network, 5 = Disc, 6 = RAM disk\r\n\r\n        if self.show_all_disks:\r\n            print(\"!WARNING!  This list includes ALL disk types.\")\r\n            print(\"!WARNING!  Be ABSOLUTELY sure before selecting\")\r\n            print(\"!WARNING!  a disk!\")\r\n        else:\r\n            print(\"!WARNING!  This list includes both Removable AND\")\r\n            print(\"!WARNING!  Unknown disk types.  Be ABSOLUTELY sure\")\r\n            print(\"!WARNING!  before selecting a disk!\")\r\n        print(\"\")\r\n        for disk in sorted(rem_disks,key=lambda x:int(x)):\r\n            print(\"{}. {} - {} ({})\".format(\r\n                disk, \r\n                rem_disks[disk].get(\"model\",\"Unknown\"), \r\n                self.dl.get_size(rem_disks[disk].get(\"size\",-1),strip_zeroes=True),\r\n                [\"Unknown\",\"No Root Dir\",\"Removable\",\"Local\",\"Network\",\"Disc\",\"RAM Disk\"][rem_disks[disk].get(\"type\",0)]\r\n                ))\r\n            if not len(rem_disks[disk].get(\"partitions\",{})):\r\n                print(\"   No Mounted Partitions\")\r\n            else:\r\n                parts = rem_disks[disk][\"partitions\"]\r\n                for p in sorted(parts,key=lambda x:int(x)):\r\n                    print(\"   {}. {} ({}) {} - {}\".format(\r\n                        p,\r\n                        parts[p].get(\"letter\",\"No Letter\"),\r\n                        \"No Name\" if not parts[p].get(\"name\",None) else parts[p].get(\"name\",\"No Name\"),\r\n                        parts[p].get(\"file system\",\"Unknown FS\"),\r\n                        self.dl.get_size(parts[p].get(\"size\",-1),strip_zeroes=True)\r\n                    ))\r\n        print(\"\")\r\n        print(\"Q. Quit\")\r\n        print(\"\")\r\n        print(\"Usage: [drive number][options] r[Clover revision (optional), requires C]\\n  (eg. 1B C r5092)\")\r\n        print(\"  Options are as follows with precedence B > F > E > U > G:\")\r\n        print(\"    B = Only install the boot manager to the drive's first partition.\")\r\n        print(\"    F = Skip formatting the disk - will install the boot manager to the first\")\r\n        print(\"        partition, and dd the recovery image to the second.\")\r\n        print(\"    E = Sets the type of the drive's first partition to EFI.\")\r\n        print(\"    U = Similar to E, but sets the type to Basic Data (useful for editing).\")\r\n        print(\"    G = Format as GPT (default is MBR).\")\r\n        print(\"    C = Use Clover instead of OpenCore.\")\r\n        print(\"    L = Provide a local archive for the boot manager - must still use C if Clover.\")\r\n        print(\"    D = Used without a drive number, toggles showing all disks (currently {}).\".format(\"ENABLED\" if self.show_all_disks else \"DISABLED\"))\r\n        print(\"\")\r\n        menu = self.u.grab(\"Please select a disk or press [enter] with no options to refresh:  \")\r\n        if not len(menu):\r\n            self.main()\r\n            return\r\n        if menu.lower() == \"q\":\r\n            self.u.custom_quit()\r\n        if menu.lower() == \"d\":\r\n            self.show_all_disks ^= True\r\n            self.main()\r\n            return\r\n        only_boot = set_efi = unset_efi = use_gpt = user_provided = no_format = False\r\n        local_file = None\r\n        use_oc = True\r\n        if \"b\" in menu.lower():\r\n            only_boot = True\r\n            menu = menu.lower().replace(\"b\",\"\")\r\n        if \"c\" in menu.lower():\r\n            use_oc = False\r\n            menu = menu.lower().replace(\"c\",\"\")\r\n        if \"o\" in menu.lower(): # Remove legacy \"o\" value\r\n            menu = menu.lower().replace(\"o\",\"\")\r\n        if \"e\" in menu.lower():\r\n            set_efi = True\r\n            menu = menu.lower().replace(\"e\",\"\")\r\n        if \"u\" in menu.lower():\r\n            unset_efi = True\r\n            menu = menu.lower().replace(\"u\",\"\")\r\n        if \"g\" in menu.lower():\r\n            use_gpt = True\r\n            menu = menu.lower().replace(\"g\",\"\")\r\n        if \"l\" in menu.lower():\r\n            user_provided = True\r\n            menu = menu.lower().replace(\"l\",\"\")\r\n        if \"f\" in menu.lower():\r\n            no_format = True\r\n            menu = menu.lower().replace(\"f\",\"\")\r\n\r\n        # Extract Clover version from args if found\r\n        clover_list = [x for x in menu.split() if x.lower().startswith(\"r\") and all(y in \"0123456789\" for y in x[1:])]\r\n        menu = \" \".join([x for x in menu.split() if not x in clover_list])\r\n        clover_version = None if not len(clover_list) else clover_list[0][1:] # Skip the \"r\" prefix\r\n\r\n        # Prepare for OC if need be\r\n        if use_oc: clover_version = \"OpenCore\"\r\n\r\n        selected_disk = rem_disks.get(menu.strip(),None)\r\n        if not selected_disk:\r\n            self.u.head(\"Invalid Choice\")\r\n            print(\"\")\r\n            print(\"Disk {} is not an option.\".format(menu))\r\n            print(\"\")\r\n            self.u.grab(\"Returning in 5 seconds...\", timeout=5)\r\n            self.main()\r\n            return\r\n        # Got a disk!\r\n        if user_provided:\r\n            # Prompt the user for the target archive\r\n            while True:\r\n                self.u.head(\"Local Archive\")\r\n                print(\"\")\r\n                if use_oc:\r\n                    print(\"NOTE:  OpenCore archives are expected to be .zip!\")\r\n                else:\r\n                    print(\"NOTE:  Clover archives are expected to be an ISO packed in either .tar.lzma or .7z!\")\r\n                print(\"\")\r\n                print(\"M. Return to the menu\")\r\n                print(\"Q. Quit\")\r\n                print(\"\")\r\n                print(\"(To copy a file's path, shift + right-click in Explorer and select 'Copy as path')\\n\")\r\n                l = self.u.grab(\"Please {} archive path here:  \".format(\"OpenCore\" if use_oc else \"Clover\"))\r\n                if not len(l):\r\n                    continue\r\n                if l.lower() == \"m\":\r\n                    break\r\n                elif l.lower() == \"q\":\r\n                    self.u.custom_quit()\r\n                l_check = self.u.check_path(l)\r\n                if not l_check or not l_check.lower().endswith(\".zip\" if use_oc else (\".tar.lzma\",\".7z\")):\r\n                    continue\r\n                # Got a valid path that ends with the proper extension\r\n                local_file = l_check\r\n                break\r\n            # Check if we got something\r\n            if not local_file:\r\n                self.main()\r\n                return\r\n        if only_boot:\r\n            if use_oc: self.install_oc(selected_disk, local_file=local_file)\r\n            else: self.install_clover(selected_disk, clover_version, local_file=local_file)\r\n        elif no_format:\r\n            # Make sure we warn the user that the second partition **NEEDS** to be a RAW\r\n            # partition for dd to properly work\r\n            while True:\r\n                self.u.head(\"WARNING\")\r\n                print(\"\")\r\n                print(\"{}. {} - {} ({})\".format(\r\n                    selected_disk.get(\"index\",-1), \r\n                    selected_disk.get(\"model\",\"Unknown\"), \r\n                    self.dl.get_size(selected_disk.get(\"size\",-1),strip_zeroes=True),\r\n                    [\"Unknown\",\"No Root Dir\",\"Removable\",\"Local\",\"Network\",\"Disc\",\"RAM Disk\"][selected_disk.get(\"type\",0)]\r\n                    ))\r\n                print(\"\")\r\n                print(\"In order to continue without formatting, the selected disk's first\")\r\n                print(\"partition MUST be FAT32, and the second MUST be RAW.  If that is not\")\r\n                print(\"the case, the operation WILL fail.\")\r\n                print(\"\")\r\n                yn = self.u.grab(\"Continue? (y/n):  \")\r\n                if yn.lower() == \"n\":\r\n                    self.main()\r\n                    return\r\n                if yn.lower() == \"y\":\r\n                    break\r\n            self.select_package(selected_disk, clover_version, local_file=local_file)\r\n        elif set_efi:\r\n            self.diskpart_flag(selected_disk, True)\r\n        elif unset_efi:\r\n            self.diskpart_flag(selected_disk, False)\r\n        else:\r\n            # Check erase\r\n            while True:\r\n                self.u.head(\"Erase {}\".format(selected_disk.get(\"model\",\"Unknown\")))\r\n                print(\"\")\r\n                print(\"{}. {} - {} ({})\".format(\r\n                    selected_disk.get(\"index\",-1), \r\n                    selected_disk.get(\"model\",\"Unknown\"), \r\n                    self.dl.get_size(selected_disk.get(\"size\",-1),strip_zeroes=True),\r\n                    [\"Unknown\",\"No Root Dir\",\"Removable\",\"Local\",\"Network\",\"Disc\",\"RAM Disk\"][selected_disk.get(\"type\",0)]\r\n                    ))\r\n                print(\"\")\r\n                print(\"If you continue - THIS DISK WILL BE ERASED\")\r\n                print(\"ALL DATA WILL BE LOST AND ALL PARTITIONS WILL\")\r\n                print(\"BE REMOVED!!!!!!!\")\r\n                print(\"\")\r\n                yn = self.u.grab(\"Continue? (y/n):  \")\r\n                if yn.lower() == \"n\":\r\n                    self.main()\r\n                    return\r\n                if yn.lower() == \"y\":\r\n                    break\r\n            # Got the OK to erase!  Let's format a diskpart script!\r\n            self.diskpart_erase(selected_disk, use_gpt, clover_version, local_file=local_file)\r\n        self.main()\r\n\r\nif __name__ == '__main__':\r\n    w = WinUSB()\r\n    w.main()\r\n"
  },
  {
    "path": "Readme.md",
    "content": "Py2/py3 script that can download macOS components direct from Apple\n\nCan 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).\n\n**NOTE:** As of macOS 11 (Big Sur), Apple has changed the way they distribute macOS, and internet recovery USBs can no longer be built via MakeInstall on Windows.  macOS versions through Catalina will still work though.\n\n**NOTE 2:** As of macOS 11 (Big Sur), Apple distributes the OS via an InstallAssistant.pkg file.  `BuildmacOSInstallApp.command` is not needed to create the install application when in macOS in this case - and you can simply run `InstallAssistant.pkg`, which will place the install app in your /Applications folder on macOS.\n\nThanks to:\n\n* 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\n* munki for his [macadmin-scripts](https://github.com/munki/macadmin-scripts)\n* timsutton for [brigadier](https://github.com/timsutton/brigadier)\n* 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\n"
  },
  {
    "path": "Scripts/__init__.py",
    "content": "from os.path import dirname, basename, isfile\r\nimport glob\r\nmodules = glob.glob(dirname(__file__)+\"/*.py\")\r\n__all__ = [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')]"
  },
  {
    "path": "Scripts/disk.py",
    "content": "import subprocess, plistlib, sys, os, time, json\nsys.path.append(os.path.abspath(os.path.dirname(os.path.realpath(__file__))))\nimport run\nif sys.version_info < (3,0):\n    # Force use of StringIO instead of cStringIO as the latter\n    # has issues with Unicode strings\n    from StringIO import StringIO\n\nclass Disk:\n\n    def __init__(self):\n        self.r = run.Run()\n        self.diskutil = self.get_diskutil()\n        self.os_version = \".\".join(\n            self.r.run({\"args\":[\"sw_vers\", \"-productVersion\"]})[0].split(\".\")[:2]\n        )\n        self.full_os_version = self.r.run({\"args\":[\"sw_vers\", \"-productVersion\"]})[0]\n        if len(self.full_os_version.split(\".\")) < 3:\n            # Add .0 in case of 10.14\n            self.full_os_version += \".0\"\n        self.sudo_mount_version = \"10.13.6\"\n        self.sudo_mount_types   = [\"efi\"]\n        self.apfs = {}\n        self._update_disks()\n\n    def _get_str(self, val):\n        # Helper method to return a string value based on input type\n        if (sys.version_info < (3,0) and isinstance(val, unicode)) or (sys.version_info >= (3,0) and isinstance(val, bytes)):\n            return val.encode(\"utf-8\")\n        return str(val)\n\n    def _get_plist(self, s):\n        p = {}\n        try:\n            if sys.version_info >= (3, 0):\n                p = plistlib.loads(s.encode(\"utf-8\"))\n            else:\n                # p = plistlib.readPlistFromString(s)\n                # We avoid using readPlistFromString() as that uses\n                # cStringIO and fails when Unicode strings are detected\n                # Don't subclass - keep the parser local\n                from xml.parsers.expat import ParserCreate\n                # Create a new PlistParser object - then we need to set up\n                # the values and parse.\n                pa = plistlib.PlistParser()\n                # We also monkey patch this to encode unicode as utf-8\n                def end_string():\n                    d = pa.getData()\n                    if isinstance(d,unicode):\n                        d = d.encode(\"utf-8\")\n                    pa.addObject(d)\n                pa.end_string = end_string\n                parser = ParserCreate()\n                parser.StartElementHandler = pa.handleBeginElement\n                parser.EndElementHandler = pa.handleEndElement\n                parser.CharacterDataHandler = pa.handleData\n                if isinstance(s, unicode):\n                    # Encode unicode -> string; use utf-8 for safety\n                    s = s.encode(\"utf-8\")\n                # Parse the string\n                parser.Parse(s, 1)\n                p = pa.root\n        except Exception as e:\n            print(e)\n            pass\n        return p\n\n    def _compare_versions(self, vers1, vers2, pad = -1):\n        # Helper method to compare ##.## strings\n        #\n        # vers1 < vers2 = True\n        # vers1 = vers2 = None\n        # vers1 > vers2 = False\n        #\n        # Must be separated with a period\n        \n        # Sanitize the pads\n        pad = -1 if not type(pad) is int else pad\n        \n        # Cast as strings\n        vers1 = str(vers1)\n        vers2 = str(vers2)\n        \n        # Split to lists\n        v1_parts = vers1.split(\".\")\n        v2_parts = vers2.split(\".\")\n        \n        # Equalize lengths\n        if len(v1_parts) < len(v2_parts):\n            v1_parts.extend([str(pad) for x in range(len(v2_parts) - len(v1_parts))])\n        elif len(v2_parts) < len(v1_parts):\n            v2_parts.extend([str(pad) for x in range(len(v1_parts) - len(v2_parts))])\n        \n        # Iterate and compare\n        for i in range(len(v1_parts)):\n            # Remove non-numeric\n            v1 = ''.join(c for c in v1_parts[i] if c.isdigit())\n            v2 = ''.join(c for c in v2_parts[i] if c.isdigit())\n            # If empty - make it a pad var\n            v1 = pad if not len(v1) else v1\n            v2 = pad if not len(v2) else v2\n            # Compare\n            if int(v1) < int(v2):\n                return True\n            elif int(v1) > int(v2):\n                return False\n        # Never differed - return None, must be equal\n        return None\n\n    def update(self):\n        self._update_disks()\n\n    def _update_disks(self):\n        self.disks = self.get_disks()\n        self.disk_text = self.get_disk_text()\n        if self._compare_versions(\"10.12\", self.os_version):\n            self.apfs = self.get_apfs()\n        else:\n            self.apfs = {}\n\n    def get_diskutil(self):\n        # Returns the path to the diskutil binary\n        return self.r.run({\"args\":[\"which\", \"diskutil\"]})[0].split(\"\\n\")[0].split(\"\\r\")[0]\n\n    def get_disks(self):\n        # Returns a dictionary object of connected disks\n        disk_list = self.r.run({\"args\":[self.diskutil, \"list\", \"-plist\"]})[0]\n        return self._get_plist(disk_list)\n\n    def get_disk_text(self):\n        # Returns plain text listing connected disks\n        return self.r.run({\"args\":[self.diskutil, \"list\"]})[0]\n\n    def get_disk_info(self, disk):\n        disk_id = self.get_identifier(disk)\n        if not disk_id:\n            return None\n        disk_list = self.r.run({\"args\":[self.diskutil, \"info\", \"-plist\", disk_id]})[0]\n        return self._get_plist(disk_list)\n\n    def get_disk_fs(self, disk):\n        disk_id = self.get_identifier(disk)\n        if not disk_id:\n            return None\n        return self.get_disk_info(disk_id).get(\"FilesystemName\", None)\n\n    def get_disk_fs_type(self, disk):\n        disk_id = self.get_identifier(disk)\n        if not disk_id:\n            return None\n        return self.get_disk_info(disk_id).get(\"FilesystemType\", None)\n\n    def get_apfs(self):\n        # Returns a dictionary object of apfs disks\n        output = self.r.run({\"args\":\"echo y | \" + self.diskutil + \" apfs list -plist\", \"shell\" : True})\n        if not output[2] == 0:\n            # Error getting apfs info - return an empty dict\n            return {}\n        disk_list = output[0]\n        p_list = disk_list.split(\"<?xml\")\n        if len(p_list) > 1:\n            # We had text before the start - get only the plist info\n            disk_list = \"<?xml\" + p_list[-1]\n        return self._get_plist(disk_list)\n\n    def is_apfs(self, disk):\n        disk_id = self.get_identifier(disk)\n        if not disk_id:\n            return None\n        # Takes a disk identifier, and returns whether or not it's apfs\n        for d in self.disks.get(\"AllDisksAndPartitions\", []):\n            if not \"APFSVolumes\" in d:\n                continue\n            if d.get(\"DeviceIdentifier\", \"\").lower() == disk_id.lower():\n                return True\n            for a in d.get(\"APFSVolumes\", []):\n                if a.get(\"DeviceIdentifier\", \"\").lower() == disk_id.lower():\n                    return True\n        return False\n\n    def is_apfs_container(self, disk):\n        disk_id = self.get_identifier(disk)\n        if not disk_id:\n            return None\n        # Takes a disk identifier, and returns whether or not that specific \n        # disk/volume is an APFS Container\n        for d in self.disks.get(\"AllDisksAndPartitions\", []):\n            # Only check partitions\n            for p in d.get(\"Partitions\", []):\n                if disk_id.lower() == p.get(\"DeviceIdentifier\", \"\").lower():\n                    return p.get(\"Content\", \"\").lower() == \"apple_apfs\"\n        return False\n\n    def is_cs_container(self, disk):\n        disk_id = self.get_identifier(disk)\n        if not disk_id:\n            return None\n        # Takes a disk identifier, and returns whether or not that specific \n        # disk/volume is an CoreStorage Container\n        for d in self.disks.get(\"AllDisksAndPartitions\", []):\n            # Only check partitions\n            for p in d.get(\"Partitions\", []):\n                if disk_id.lower() == p.get(\"DeviceIdentifier\", \"\").lower():\n                    return p.get(\"Content\", \"\").lower() == \"apple_corestorage\"\n        return False\n\n    def is_core_storage(self, disk):\n        disk_id = self.get_identifier(disk)\n        if not disk_id:\n            return None\n        if self._get_physical_disk(disk_id, \"Logical Volume on \"):\n            return True\n        return False\n\n    def get_identifier(self, disk):\n        # Should be able to take a mount point, disk name, or disk identifier,\n        # and return the disk's identifier\n        # Iterate!!\n        if not disk or not len(self._get_str(disk)):\n            return None\n        disk = self._get_str(disk).lower()\n        if disk.startswith(\"/dev/r\"):\n            disk = disk[len(\"/dev/r\"):]\n        elif disk.startswith(\"/dev/\"):\n            disk = disk[len(\"/dev/\"):]\n        if disk in self.disks.get(\"AllDisks\", []):\n            return disk\n        for d in self.disks.get(\"AllDisksAndPartitions\", []):\n            for a in d.get(\"APFSVolumes\", []):\n                if disk in [ self._get_str(a.get(x, \"\")).lower() for x in [\"DeviceIdentifier\", \"VolumeName\", \"VolumeUUID\", \"DiskUUID\", \"MountPoint\"] ]:\n                    return a.get(\"DeviceIdentifier\", None)\n            for a in d.get(\"Partitions\", []):\n                if disk in [ self._get_str(a.get(x, \"\")).lower() for x in [\"DeviceIdentifier\", \"VolumeName\", \"VolumeUUID\", \"DiskUUID\", \"MountPoint\"] ]:\n                    return a.get(\"DeviceIdentifier\", None)\n        # At this point, we didn't find it\n        return None\n\n    def get_top_identifier(self, disk):\n        disk_id = self.get_identifier(disk)\n        if not disk_id:\n            return None\n        return disk_id.replace(\"disk\", \"didk\").split(\"s\")[0].replace(\"didk\", \"disk\")\n        \n    def _get_physical_disk(self, disk, search_term):\n        # Change disk0s1 to disk0\n        our_disk = self.get_top_identifier(disk)\n        our_term = \"/dev/\" + our_disk\n        found_disk = False\n        our_text = \"\"\n        for line in self.disk_text.split(\"\\n\"):\n            if line.lower().startswith(our_term):\n                found_disk = True\n                continue\n            if not found_disk:\n                continue\n            if line.lower().startswith(\"/dev/disk\"):\n                # At the next disk - bail\n                break\n            if search_term.lower() in line.lower():\n                our_text = line\n                break\n        if not len(our_text):\n            # Nothing found\n            return None\n        our_stores = \"\".join(our_text.strip().split(search_term)[1:]).split(\" ,\")\n        if not len(our_stores):\n            return None\n        for store in our_stores:\n            efi = self.get_efi(store)\n            if efi:\n                return store\n        return None\n\n    def get_physical_store(self, disk):\n        # Returns the physical store containing the EFI\n        disk_id = self.get_identifier(disk)\n        if not disk_id:\n            return None\n        if not self.is_apfs(disk_id):\n            return None\n        return self._get_physical_disk(disk_id, \"Physical Store \")\n\n    def get_core_storage_pv(self, disk):\n        # Returns the core storage physical volume containing the EFI\n        disk_id = self.get_identifier(disk)\n        if not disk_id:\n            return None\n        if not self.is_core_storage(disk_id):\n            return None\n        return self._get_physical_disk(disk_id, \"Logical Volume on \")\n\n    def get_parent(self, disk):\n        # Disk can be a mount point, disk name, or disk identifier\n        disk_id = self.get_identifier(disk)\n        if self.is_apfs(disk_id):\n            disk_id = self.get_physical_store(disk_id)\n        elif self.is_core_storage(disk_id):\n            disk_id = self.get_core_storage_pv(disk_id)\n        if not disk_id:\n            return None\n        if self.is_apfs(disk_id):\n            # We have apfs - let's get the container ref\n            for a in self.apfs.get(\"Containers\", []):\n                # Check if it's the whole container\n                if a.get(\"ContainerReference\", \"\").lower() == disk_id.lower():\n                    return a[\"ContainerReference\"]\n                # Check through each volume and return the parent's container ref\n                for v in a.get(\"Volumes\", []):\n                    if v.get(\"DeviceIdentifier\", \"\").lower() == disk_id.lower():\n                        return a.get(\"ContainerReference\", None)\n        else:\n            # Not apfs - go through all volumes and whole disks\n            for d in self.disks.get(\"AllDisksAndPartitions\", []):\n                if d.get(\"DeviceIdentifier\", \"\").lower() == disk_id.lower():\n                    return d[\"DeviceIdentifier\"]\n                for p in d.get(\"Partitions\", []):\n                    if p.get(\"DeviceIdentifier\", \"\").lower() == disk_id.lower():\n                        return d[\"DeviceIdentifier\"]\n        # Didn't find anything\n        return None\n\n    def get_efi(self, disk):\n        disk_id = self.get_parent(self.get_identifier(disk))\n        if not disk_id:\n            return None\n        # At this point - we should have the parent\n        for d in self.disks[\"AllDisksAndPartitions\"]:\n            if d.get(\"DeviceIdentifier\", \"\").lower() == disk_id.lower():\n                # Found our disk\n                for p in d.get(\"Partitions\", []):\n                    if p.get(\"Content\", \"\").lower() == \"efi\":\n                        return p.get(\"DeviceIdentifier\", None)\n        return None\n\n    def mount_partition(self, disk):\n        disk_id = self.get_identifier(disk)\n        if not disk_id:\n            return None\n        sudo = False\n        if not self._compare_versions(self.full_os_version, self.sudo_mount_version) and self.get_content(disk_id).lower() in self.sudo_mount_types:\n            sudo = True\n        out = self.r.run({\"args\":[self.diskutil, \"mount\", disk_id], \"sudo\":sudo})\n        self._update_disks()\n        return out\n\n    def unmount_partition(self, disk):\n        disk_id = self.get_identifier(disk)\n        if not disk_id:\n            return None\n        out = self.r.run({\"args\":[self.diskutil, \"unmount\", disk_id]})\n        self._update_disks()\n        return out\n\n    def is_mounted(self, disk):\n        disk_id = self.get_identifier(disk)\n        if not disk_id:\n            return None\n        m = self.get_mount_point(disk_id)\n        return (m != None and len(m))\n\n    def get_volumes(self):\n        # Returns a list object with all volumes from disks\n        return self.disks.get(\"VolumesFromDisks\", [])\n\n    def _get_value_apfs(self, disk, field, default = None):\n        return self._get_value(disk, field, default, True)\n\n    def _get_value(self, disk, field, default = None, apfs_only = False):\n        disk_id = self.get_identifier(disk)\n        if not disk_id:\n            return None\n        # Takes a disk identifier, and returns the requested value\n        for d in self.disks.get(\"AllDisksAndPartitions\", []):\n            for a in d.get(\"APFSVolumes\", []):\n                if a.get(\"DeviceIdentifier\", \"\").lower() == disk_id.lower():\n                    return a.get(field, default)\n            if apfs_only:\n                # Skip looking at regular partitions\n                continue\n            if d.get(\"DeviceIdentifier\", \"\").lower() == disk_id.lower():\n                return d.get(field, default)\n            for a in d.get(\"Partitions\", []):\n                if a.get(\"DeviceIdentifier\", \"\").lower() == disk_id.lower():\n                    return a.get(field, default)\n        return None\n\n    # Getter methods\n    def get_content(self, disk):\n        return self._get_value(disk, \"Content\")\n\n    def get_volume_name(self, disk):\n        return self._get_value(disk, \"VolumeName\")\n\n    def get_volume_uuid(self, disk):\n        return self._get_value(disk, \"VolumeUUID\")\n\n    def get_disk_uuid(self, disk):\n        return self._get_value(disk, \"DiskUUID\")\n\n    def get_mount_point(self, disk):\n        return self._get_value(disk, \"MountPoint\")\n\n    def open_mount_point(self, disk, new_window = False):\n        disk_id = self.get_identifier(disk)\n        if not disk_id:\n            return None\n        mount = self.get_mount_point(disk_id)\n        if not mount:\n            return None\n        out = self.r.run({\"args\":[\"open\", mount]})\n        return out[2] == 0\n\n    def get_mounted_volumes(self):\n        # Returns a list of mounted volumes\n        vol_list = self.r.run({\"args\":[\"ls\", \"-1\", \"/Volumes\"]})[0].split(\"\\n\")\n        vol_list = [ x for x in vol_list if x != \"\" ]\n        return vol_list\n\n    def get_mounted_volume_dicts(self):\n        # Returns a list of dicts of name, identifier, mount point dicts\n        vol_list = []\n        for v in self.get_mounted_volumes():\n            i = self.get_identifier(os.path.join(\"/Volumes\", v))\n            if i == None:\n                i = self.get_identifier(\"/\")\n                if not self.get_volume_name(i) == v:\n                    # Not valid and not our boot drive\n                    continue\n            vol_list.append({\n                \"name\" : self.get_volume_name(i),\n                \"identifier\" : i,\n                \"mount_point\" : self.get_mount_point(i),\n                \"disk_uuid\" : self.get_disk_uuid(i),\n                \"volume_uuid\" : self.get_volume_uuid(i)\n            })\n        return vol_list\n\n    def get_disks_and_partitions_dict(self):\n        # Returns a list of dictionaries like so:\n        # { \"disk0\" : { \"partitions\" : [ \n        #    { \n        #      \"identifier\" : \"disk0s1\", \n        #      \"name\" : \"EFI\", \n        #      \"mount_point\" : \"/Volumes/EFI\"\n        #     } \n        #  ] } }\n        disks = {}\n        for d in self.disks.get(\"AllDisks\", []):\n            # Get the parent and make sure it has an entry\n            parent     = self.get_parent(d)\n            top_disk   = self.get_top_identifier(d)\n            if top_disk == d and not self.is_core_storage(d):\n                # Top level, skip\n                continue\n            # Not top level - make sure it's not an apfs container or core storage container\n            if self.is_apfs_container(d):\n                continue\n            if self.is_cs_container(d):\n                continue\n            if not parent in disks:\n                disks[parent] = { \"partitions\" : [] }\n            disks[parent][\"partitions\"].append({\n                \"name\" : self.get_volume_name(d),\n                \"identifier\" : d,\n                \"mount_point\" : self.get_mount_point(d),\n                \"disk_uuid\" : self.get_disk_uuid(d),\n                \"volume_uuid\" : self.get_volume_uuid(d)\n            })\n        return disks\n"
  },
  {
    "path": "Scripts/diskwin.py",
    "content": "import subprocess, plistlib, sys, os, time, json, csv\nsys.path.append(os.path.abspath(os.path.dirname(os.path.realpath(__file__))))\nfrom Scripts import run\n\nclass Disk:\n\n    def __init__(self):\n        self.r = run.Run()\n        self.wmic = self._get_wmic()\n        if self.wmic and not os.path.exists(self.wmic):\n            self.wmic = None\n        self.disks = {}\n        self._update_disks()\n\n    def _get_wmic(self):\n        # Attempt to locate WMIC.exe\n        wmic_list = self.r.run({\"args\":[\"where\",\"wmic\"]})[0].replace(\"\\r\",\"\").split(\"\\n\")\n        if wmic_list:\n            return wmic_list[0]\n        return None\n\n    def update(self):\n        self._update_disks()\n\n    def _update_disks(self):\n        self.disks = self.get_disks()\n\n    def _get_rows(self, row_list):\n        rows = []\n        last_row = []\n        for row in row_list:\n            if not row.strip(): # Empty\n                if last_row: # Got a row at least - append it and reset\n                    rows.append(last_row)\n                    last_row = []\n                continue # Skip anything else\n            # Not an empty row - let's try to get the info\n            try: last_row.append(\" : \".join(row.split(\" : \")[1:]))\n            except: pass\n        return rows\n\n    def _get_diskdrive(self):\n        disks = []\n        if self.wmic: # Use WMIC where possible\n            try:\n                wmic = self.r.run({\"args\":[self.wmic, \"DiskDrive\", \"get\", \"DeviceID,Index,Model,Partitions,Size\", \"/format:csv\"]})[0]\n                # Get the rows - but skip the first 2 (empty, headers) and the last 1 (empty again)\n                disks = list(csv.reader(wmic.replace(\"\\r\",\"\").split(\"\\n\"), delimiter=\",\"))[2:-1]\n                # We need to skip the Node value for each row as well\n                disks = [x[1:] for x in disks]\n            except:\n                pass\n        if not disks: # Use PowerShell and parse the info manually\n            try:\n                ps = self.r.run({\"args\":[\"powershell\", \"-c\", \"Get-WmiObject -Class Win32_DiskDrive | Format-List -Property DeviceID,Index,Model,Partitions,Size\"]})[0]\n                # We need to iterate the rows and add each column manually\n                disks = self._get_rows(ps.replace(\"\\r\",\"\").split(\"\\n\"))\n            except:\n                pass\n        return disks\n\n    def _get_ldtop(self):\n        disks = []\n        if self.wmic: # Use WMIC where possible\n            try:\n                wmic = self.r.run({\"args\":[self.wmic, \"path\", \"Win32_LogicalDiskToPartition\", \"get\", \"Antecedent,Dependent\"]})[0]\n                # Get the rows - but skip the first and last as they're empty\n                disks = wmic.replace(\"\\r\",\"\").split(\"\\n\")[1:-1]\n            except:\n                pass\n        if not disks: # Use PowerShell and parse the info manually\n            try:\n                ps = self.r.run({\"args\":[\"powershell\", \"-c\", \"Get-WmiObject -Class Win32_LogicalDiskToPartition | Format-List -Property Antecedent,Dependent\"]})[0]\n                # We need to iterate the rows and add each column manually\n                disks = self._get_rows(ps.replace(\"\\r\",\"\").split(\"\\n\"))\n                # We need to join the values with 2 spaces to match the WMIC output\n                disks = [\"  \".join(x) for x in disks]\n            except:\n                pass\n        return disks\n\n    def _get_logicaldisk(self):\n        disks = []\n        if self.wmic: # Use WMIC where possible\n            try:\n                wmic = self.r.run({\"args\":[self.wmic, \"LogicalDisk\", \"get\", \"DeviceID,DriveType,FileSystem,Size,VolumeName\", \"/format:csv\"]})[0]\n                # Get the rows - but skip the first 2 (empty, headers) and the last 1 (empty again)\n                disks = list(csv.reader(wmic.replace(\"\\r\",\"\").split(\"\\n\"), delimiter=\",\"))[2:-1]\n                # We need to skip the Node value for each row as well\n                disks = [x[1:] for x in disks]\n            except:\n                pass\n        if not disks: # Use PowerShell and parse the info manually\n            try:\n                ps = self.r.run({\"args\":[\"powershell\", \"-c\", \"Get-WmiObject -Class Win32_LogicalDisk | Format-List -Property DeviceID,DriveType,FileSystem,Size,VolumeName\"]})[0]\n                # We need to iterate the rows and add each column manually\n                disks = self._get_rows(ps.replace(\"\\r\",\"\").split(\"\\n\"))\n            except:\n                pass\n        return disks\n\n    def get_disks(self):\n        # We hate windows... all of us.\n        #\n        # This has to be done in 3 commands,\n        # 1. To get the PHYSICALDISK entries, index, and model\n        # 2. To get the drive letter, volume name, fs, and size\n        # 3. To get some connection between them...\n        #\n        # May you all forgive me...\n\n        disks = self._get_diskdrive()\n        p_disks = {}\n        for ds in disks:\n            if len(ds) < 5:\n                continue\n            p_disks[ds[1]] = {\n                \"device\":ds[0],\n                \"model\":\" \".join(ds[2:-2]),\n                \"type\":0 # 0 = Unknown, 1 = No Root Dir, 2 = Removable, 3 = Local, 4 = Network, 5 = Disc, 6 = RAM disk\n                }\n            # More fault-tolerance with ints\n            p_disks[ds[1]][\"index\"] = int(ds[1]) if len(ds[1]) else -1\n            p_disks[ds[1]][\"size\"] = int(ds[-1]) if len(ds[-1]) else -1\n            p_disks[ds[1]][\"partitioncount\"] = int(ds[-2]) if len(ds[-2]) else 0\n            \n        if not p_disks:\n            # Drat, nothing\n            return p_disks\n        # Let's find a way to map this biz now\n        ldtop = self._get_ldtop()\n        for l in ldtop:\n            l = l.lower()\n            d = p = mp = None\n            try:\n                dp = l.split(\"deviceid=\")[1].split('\"')[1]\n                mp = l.split(\"deviceid=\")[-1].split('\"')[1].upper()\n                d = dp.split(\"disk #\")[1].split(\",\")[0]\n                p = dp.split(\"partition #\")[1]\n            except:\n                pass\n            if any([d, p, mp]):\n                # Got *something*\n                if p_disks.get(d,None):\n                    if not p_disks[d].get(\"partitions\",None):\n                        p_disks[d][\"partitions\"] = {}\n                    p_disks[d][\"partitions\"][p] = {\"letter\":mp}\n        # Last attempt to do this - let's get the partition names!\n        parts = self._get_logicaldisk()\n        if not parts:\n            return p_disks\n        for ps in parts:\n            if len(ps) < 2:\n                # Need the drive letter and disk type at minimum\n                continue\n            # Organize!\n            plt = ps[0] # get letter\n            ptp = ps[1] # get disk type\n            # Initialize\n            pfs = pnm = None\n            psz = -1 # Set to -1 initially for indeterminate size\n            try:\n                pfs = ps[2] # get file system\n                psz = ps[3] # get size\n                pnm = ps[4] # get the rest in the name\n            except:\n                pass\n            for d in p_disks:\n                p_dict = p_disks[d]\n                for pr in p_dict.get(\"partitions\",{}):\n                    pr = p_dict[\"partitions\"][pr]\n                    if pr.get(\"letter\",\"\").upper() == plt.upper():\n                        # Found it - set all attributes\n                        pr[\"size\"] = int(psz) if len(psz) else -1\n                        pr[\"file system\"] = pfs\n                        pr[\"name\"] = pnm\n                        # Also need to set the parent drive's type\n                        if len(ptp):\n                            p_dict[\"type\"] = int(ptp)\n                        break\n        return p_disks\n"
  },
  {
    "path": "Scripts/downloader.py",
    "content": "import sys, os, time, ssl, gzip, multiprocessing\nfrom io import BytesIO\n# Python-aware urllib stuff\ntry:\n    from urllib.request import urlopen, Request\n    import queue as q\nexcept ImportError:\n    # Import urllib2 to catch errors\n    import urllib2\n    from urllib2 import urlopen, Request\n    import Queue as q\n\nTERMINAL_WIDTH = 120 if os.name==\"nt\" else 80\n\ndef get_size(size, suffix=None, use_1024=False, round_to=2, strip_zeroes=False):\n    # size is the number of bytes\n    # suffix is the target suffix to locate (B, KB, MB, etc) - if found\n    # use_2014 denotes whether or not we display in MiB vs MB\n    # round_to is the number of dedimal points to round our result to (0-15)\n    # strip_zeroes denotes whether we strip out zeroes \n\n    # Failsafe in case our size is unknown\n    if size == -1:\n        return \"Unknown\"\n    # Get our suffixes based on use_1024\n    ext = [\"B\",\"KiB\",\"MiB\",\"GiB\",\"TiB\",\"PiB\"] if use_1024 else [\"B\",\"KB\",\"MB\",\"GB\",\"TB\",\"PB\"]\n    div = 1024 if use_1024 else 1000\n    s = float(size)\n    s_dict = {} # Initialize our dict\n    # Iterate the ext list, and divide by 1000 or 1024 each time to setup the dict {ext:val}\n    for e in ext:\n        s_dict[e] = s\n        s /= div\n    # Get our suffix if provided - will be set to None if not found, or if started as None\n    suffix = next((x for x in ext if x.lower() == suffix.lower()),None) if suffix else suffix\n    # Get the largest value that's still over 1\n    biggest = suffix if suffix else next((x for x in ext[::-1] if s_dict[x] >= 1), \"B\")\n    # Determine our rounding approach - first make sure it's an int; default to 2 on error\n    try:round_to=int(round_to)\n    except:round_to=2\n    round_to = 0 if round_to < 0 else 15 if round_to > 15 else round_to # Ensure it's between 0 and 15\n    bval = round(s_dict[biggest], round_to)\n    # Split our number based on decimal points\n    a,b = str(bval).split(\".\")\n    # Check if we need to strip or pad zeroes\n    b = b.rstrip(\"0\") if strip_zeroes else b.ljust(round_to,\"0\") if round_to > 0 else \"\"\n    return \"{:,}{} {}\".format(int(a),\"\" if not b else \".\"+b,biggest)\n\ndef _process_hook(queue, total_size, bytes_so_far=0, update_interval=1.0, max_packets=0):\n    packets = []\n    speed = remaining = \"\"\n    last_update = time.time()\n    while True:\n        # Write our info first so we have *some* status while\n        # waiting for packets\n        if total_size > 0:\n            percent = float(bytes_so_far) / total_size\n            percent = round(percent*100, 2)\n            t_s = get_size(total_size)\n            try:\n                b_s = get_size(bytes_so_far, t_s.split(\" \")[1])\n            except:\n                b_s = get_size(bytes_so_far)\n            perc_str = \" {:.2f}%\".format(percent)\n            bar_width = (TERMINAL_WIDTH // 3)-len(perc_str)\n            progress = \"=\" * int(bar_width * (percent/100))\n            sys.stdout.write(\"\\r\\033[K{}/{} | {}{}{}{}{}\".format(\n                b_s,\n                t_s,\n                progress,\n                \" \" * (bar_width-len(progress)),\n                perc_str,\n                speed,\n                remaining\n            ))\n        else:\n            b_s = get_size(bytes_so_far)\n            sys.stdout.write(\"\\r\\033[K{}{}\".format(b_s, speed))\n        sys.stdout.flush()\n        # Now we gather the next packet\n        try:\n            packet = queue.get(timeout=update_interval)\n            # Packets should be formatted as a tuple of\n            # (timestamp, len(bytes_downloaded))\n            # If \"DONE\" is passed, we assume the download\n            # finished - and bail\n            if packet == \"DONE\":\n                print(\"\") # Jump to the next line\n                return\n            # Append our packet to the list and ensure we're not\n            # beyond our max.\n            # Only check max if it's > 0\n            packets.append(packet)\n            if max_packets > 0:\n                packets = packets[-max_packets:]\n            # Increment our bytes so far as well\n            bytes_so_far += packet[1]\n        except q.Empty:\n            # Didn't get anything - reset the speed\n            # and packets\n            packets = []\n            speed = \" | 0 B/s\"\n            remaining = \" | ?? left\" if total_size > 0 else \"\"\n        except KeyboardInterrupt:\n            print(\"\") # Jump to the next line\n            return\n        # If we have packets and it's time for an update, process\n        # the info.\n        update_check = time.time()\n        if packets and update_check - last_update >= update_interval:\n            last_update = update_check # Refresh our update timestamp\n            speed = \" | ?? B/s\"\n            if len(packets) > 1:\n                # Let's calculate the amount downloaded over how long\n                try:\n                    first,last = packets[0][0],packets[-1][0]\n                    chunks = sum([float(x[1]) for x in packets])\n                    t = last-first\n                    assert t >= 0\n                    bytes_speed = 1. / t * chunks\n                    speed = \" | {}/s\".format(get_size(bytes_speed,round_to=1))\n                    # Get our remaining time\n                    if total_size > 0:\n                        seconds_left = (total_size-bytes_so_far) / bytes_speed\n                        days  = seconds_left // 86400\n                        hours = (seconds_left - (days*86400)) // 3600\n                        mins  = (seconds_left - (days*86400) - (hours*3600)) // 60\n                        secs  = seconds_left - (days*86400) - (hours*3600) - (mins*60)\n                        if days > 99 or bytes_speed == 0:\n                            remaining = \" | ?? left\"\n                        else:\n                            remaining = \" | {}{:02d}:{:02d}:{:02d} left\".format(\n                                \"{}:\".format(int(days)) if days else \"\",\n                                int(hours),\n                                int(mins),\n                                int(round(secs))\n                            )\n                except:\n                    pass\n                # Clear the packets so we don't reuse the same ones\n                packets = []\n\nclass Downloader:\n\n    def __init__(self,**kwargs):\n        self.ua = kwargs.get(\"useragent\",{\"User-Agent\":\"Mozilla\"})\n        self.chunk = None # Auto-assign if None, otherwise explicit\n        self.min_chunk = 1024 # 1 KiB min chunk size\n        self.max_chunk = 1024 * 1024 * 4 # 4 MiB max chunk size\n        self.chunk_rate = 0.1 # Update every 0.1 seconds\n        self.chunk_growth = 1.5 # Max multiplier for chunk growth\n        if os.name==\"nt\": os.system(\"color\") # Initialize cmd for ANSI escapes\n        # Provide reasonable default logic to workaround macOS CA file handling \n        cafile = ssl.get_default_verify_paths().openssl_cafile\n        try:\n            # If default OpenSSL CA file does not exist, use that from certifi\n            if not os.path.exists(cafile):\n                import certifi\n                cafile = certifi.where()\n            self.ssl_context = ssl.create_default_context(cafile=cafile)\n        except:\n            # None of the above worked, disable certificate verification for now\n            self.ssl_context = ssl._create_unverified_context()\n        return\n\n    def _decode(self, value, encoding=\"utf-8\", errors=\"ignore\"):\n        # Helper method to only decode if bytes type\n        if sys.version_info >= (3,0) and isinstance(value, bytes):\n            return value.decode(encoding,errors)\n        return value\n\n    def _update_main_name(self):\n        # Windows running python 2 seems to have issues with multiprocessing\n        # if the case of the main script's name is incorrect:\n        # e.g. Downloader.py vs downloader.py\n        #\n        # To work around this, we try to scrape for the correct case if\n        # possible.\n        try:\n            path = os.path.abspath(sys.modules[\"__main__\"].__file__)\n        except AttributeError as e:\n            # This likely means we're running from the interpreter\n            # directly\n            return None\n        if not os.path.isfile(path):\n            return None\n        # Get the file name and folder path\n        name = os.path.basename(path).lower()\n        fldr = os.path.dirname(path)\n        # Walk the files in the folder until we find our\n        # name - then steal its case and update that path\n        for f in os.listdir(fldr):\n            if f.lower() == name:\n                # Got it\n                new_path = os.path.join(fldr,f)\n                sys.modules[\"__main__\"].__file__ = new_path\n                return new_path\n        # If we got here, it wasn't found\n        return None\n\n    def _get_headers(self, headers = None):\n        # Fall back on the default ua if none provided\n        target = headers if isinstance(headers,dict) else self.ua\n        new_headers = {}\n        # Shallow copy to prevent changes to the headers\n        # overriding the original\n        for k in target:\n            new_headers[k] = target[k]\n        return new_headers\n\n    def open_url(self, url, headers = None):\n        headers = self._get_headers(headers)\n        # Wrap up the try/except block so we don't have to do this for each function\n        try:\n            response = urlopen(Request(url, headers=headers), context=self.ssl_context)\n        except Exception as e:\n            # No fixing this - bail\n            return None\n        return response\n\n    def get_size(self, *args, **kwargs):\n        return get_size(*args,**kwargs)\n\n    def get_string(self, url, progress = True, headers = None, expand_gzip = True):\n        response = self.get_bytes(url,progress,headers,expand_gzip)\n        if response is None: return None\n        return self._decode(response)\n\n    def get_bytes(self, url, progress = True, headers = None, expand_gzip = True):\n        response = self.open_url(url, headers)\n        if response is None: return None\n        try: total_size = int(response.headers['Content-Length'])\n        except: total_size = -1\n        chunk_so_far = b\"\"\n        packets = queue = process = None\n        if progress:\n            # Make sure our vars are initialized\n            packets = [] if progress else None\n            queue = multiprocessing.Queue()\n            # Create the multiprocess and start it\n            process = multiprocessing.Process(\n                target=_process_hook,\n                args=(queue,total_size)\n            )\n            process.daemon = True\n            # Filthy hack for earlier python versions on Windows\n            if os.name == \"nt\" and hasattr(multiprocessing,\"forking\"):\n                self._update_main_name()\n            process.start()\n        try:\n            chunk_size = self.chunk or 1024\n            auto_chunk_size = not self.chunk\n            while True:\n                t = time.perf_counter()\n                chunk = response.read(chunk_size)\n                chunk_time = time.perf_counter()-t\n                if progress:\n                    # Add our items to the queue\n                    queue.put((time.time(),len(chunk)))\n                if not chunk: break\n                chunk_so_far += chunk\n                if auto_chunk_size:\n                    # Adjust our chunk size based on the internet speed at our defined rate\n                    chunk_rate = int(len(chunk) / chunk_time * self.chunk_rate)\n                    chunk_change_max = round(chunk_size * self.chunk_growth)\n                    chunk_rate_clamped = min(max(self.min_chunk, chunk_rate), chunk_change_max)\n                    chunk_size = min(chunk_rate_clamped, self.max_chunk)\n        finally:\n            # Close the response whenever we're done\n            response.close()\n        if expand_gzip and response.headers.get(\"Content-Encoding\",\"unknown\").lower() == \"gzip\":\n            fileobj = BytesIO(chunk_so_far)\n            gfile   = gzip.GzipFile(fileobj=fileobj)\n            return gfile.read()\n        if progress:\n            # Finalize the queue and wait\n            queue.put(\"DONE\")\n            process.join()\n        return chunk_so_far\n\n    def stream_to_file(self, url, file_path, progress = True, headers = None, ensure_size_if_present = True, allow_resume = False):\n        response = self.open_url(url, headers)\n        if response is None: return None\n        bytes_so_far = 0\n        try: total_size = int(response.headers['Content-Length'])\n        except: total_size = -1\n        packets = queue = process = None\n        mode = \"wb\"\n        if allow_resume and os.path.isfile(file_path) and total_size != -1:\n            # File exists, we're resuming and have a target size.  Check the\n            # local file size.\n            current_size = os.stat(file_path).st_size\n            if current_size == total_size:\n                # File is already complete - return the path\n                return file_path\n            elif current_size < total_size:\n                response.close()\n                # File is not complete - seek to our current size\n                bytes_so_far = current_size\n                mode = \"ab\" # Append\n                # We also need to try creating a new request\n                # in order to pass our range header\n                new_headers = self._get_headers(headers)\n                # Get the start byte, 0-indexed\n                byte_string = \"bytes={}-\".format(current_size)\n                new_headers[\"Range\"] = byte_string\n                response = self.open_url(url, new_headers)\n                if response is None: return None\n        if progress:\n            # Make sure our vars are initialized\n            packets = [] if progress else None\n            queue = multiprocessing.Queue()\n            # Create the multiprocess and start it\n            process = multiprocessing.Process(\n                target=_process_hook,\n                args=(queue,total_size,bytes_so_far)\n            )\n            process.daemon = True\n            # Filthy hack for earlier python versions on Windows\n            if os.name == \"nt\" and hasattr(multiprocessing,\"forking\"):\n                self._update_main_name()\n            process.start()\n        with open(file_path,mode) as f:\n            chunk_size = self.chunk or 1024\n            auto_chunk_size = not self.chunk\n            try:\n                while True:\n                    t = time.perf_counter()\n                    chunk = response.read(chunk_size)\n                    chunk_time = time.perf_counter()-t\n                    bytes_so_far += len(chunk)\n                    if progress:\n                        # Add our items to the queue\n                        queue.put((time.time(),len(chunk)))\n                    if not chunk: break\n                    f.write(chunk)\n                    if auto_chunk_size:\n                        # Adjust our chunk size based on the internet speed at our defined rate\n                        chunk_rate = int(len(chunk) / chunk_time * self.chunk_rate)\n                        chunk_change_max = round(chunk_size * self.chunk_growth)\n                        chunk_rate_clamped = min(max(self.min_chunk, chunk_rate), chunk_change_max)\n                        chunk_size = min(chunk_rate_clamped, self.max_chunk)\n            finally:\n                # Close the response whenever we're done\n                response.close()\n        if progress:\n            # Finalize the queue and wait\n            queue.put(\"DONE\")\n            process.join()\n        if ensure_size_if_present and total_size != -1:\n            # We're verifying size - make sure we got what we asked for\n            if bytes_so_far != total_size:\n                return None # We didn't - imply it failed\n        return file_path if os.path.exists(file_path) else None\n"
  },
  {
    "path": "Scripts/plist.py",
    "content": "###     ###\n# Imports #\n###     ###\n\nimport datetime, os, plistlib, struct, sys, itertools, binascii\nfrom io import BytesIO\n\nif sys.version_info < (3,0):\n    # Force use of StringIO instead of cStringIO as the latter\n    # has issues with Unicode strings\n    from StringIO import StringIO\nelse:\n    from io import StringIO\n\ntry:\n    basestring  # Python 2\n    unicode\nexcept NameError:\n    basestring = str  # Python 3\n    unicode = str\n\ntry:\n    FMT_XML = plistlib.FMT_XML\n    FMT_BINARY = plistlib.FMT_BINARY\nexcept AttributeError:\n    FMT_XML = \"FMT_XML\"\n    FMT_BINARY = \"FMT_BINARY\"\n\n###            ###\n# Helper Methods #\n###            ###\n\ndef wrap_data(value):\n    if not _check_py3(): return plistlib.Data(value)\n    return value\n\ndef extract_data(value):\n    if not _check_py3() and isinstance(value,plistlib.Data): return value.data\n    return value\n\ndef _check_py3():\n    return sys.version_info >= (3, 0)\n\ndef _is_binary(fp):\n    if isinstance(fp, basestring):\n        return fp.startswith(b\"bplist00\")\n    header = fp.read(32)\n    fp.seek(0)\n    return header[:8] == b'bplist00'\n\ndef _seek_past_whitespace(fp):\n    offset = 0\n    while True:\n        byte = fp.read(1)\n        if not byte:\n            # End of file, reset offset and bail\n            offset = 0\n            break\n        if not byte.isspace():\n            # Found our first non-whitespace character\n            break\n        offset += 1\n    # Seek to the first non-whitespace char\n    fp.seek(offset)\n    return offset\n\n###                             ###\n# Deprecated Functions - Remapped #\n###                             ###\n\ndef readPlist(pathOrFile):\n    if not isinstance(pathOrFile, basestring):\n        return load(pathOrFile)\n    with open(pathOrFile, \"rb\") as f:\n        return load(f)\n\ndef writePlist(value, pathOrFile):\n    if not isinstance(pathOrFile, basestring):\n        return dump(value, pathOrFile, fmt=FMT_XML, sort_keys=True, skipkeys=False)\n    with open(pathOrFile, \"wb\") as f:\n        return dump(value, f, fmt=FMT_XML, sort_keys=True, skipkeys=False)\n\n###                ###\n# Remapped Functions #\n###                ###\n\ndef load(fp, fmt=None, use_builtin_types=None, dict_type=dict):\n    if _is_binary(fp):\n        use_builtin_types = False if use_builtin_types is None else use_builtin_types\n        try:\n            p = _BinaryPlistParser(use_builtin_types=use_builtin_types, dict_type=dict_type)\n        except:\n            # Python 3.9 removed use_builtin_types\n            p = _BinaryPlistParser(dict_type=dict_type)\n        return p.parse(fp)\n    elif _check_py3():\n        offset = _seek_past_whitespace(fp)\n        use_builtin_types = True if use_builtin_types is None else use_builtin_types\n        # We need to monkey patch this to allow for hex integers - code taken/modified from \n        # https://github.com/python/cpython/blob/3.8/Lib/plistlib.py\n        if fmt is None:\n            header = fp.read(32)\n            fp.seek(offset)\n            for info in plistlib._FORMATS.values():\n                if info['detect'](header):\n                    P = info['parser']\n                    break\n            else:\n                raise plistlib.InvalidFileException()\n        else:\n            P = plistlib._FORMATS[fmt]['parser']\n        try:\n            p = P(use_builtin_types=use_builtin_types, dict_type=dict_type)\n        except:\n            # Python 3.9 removed use_builtin_types\n            p = P(dict_type=dict_type)\n        if isinstance(p,plistlib._PlistParser):\n            # Monkey patch!\n            def end_integer():\n                d = p.get_data()\n                value = int(d,16) if d.lower().startswith(\"0x\") else int(d)\n                if -1 << 63 <= value < 1 << 64:\n                    p.add_object(value)\n                else:\n                    raise OverflowError(\"Integer overflow at line {}\".format(p.parser.CurrentLineNumber))\n            def end_data():\n                try:\n                    p.add_object(plistlib._decode_base64(p.get_data()))\n                except Exception as e:\n                    raise Exception(\"Data error at line {}: {}\".format(p.parser.CurrentLineNumber,e))\n            p.end_integer = end_integer\n            p.end_data = end_data\n        return p.parse(fp)\n    else:\n        offset = _seek_past_whitespace(fp)\n        # Is not binary - assume a string - and try to load\n        # We avoid using readPlistFromString() as that uses\n        # cStringIO and fails when Unicode strings are detected\n        # Don't subclass - keep the parser local\n        from xml.parsers.expat import ParserCreate\n        # Create a new PlistParser object - then we need to set up\n        # the values and parse.\n        p = plistlib.PlistParser()\n        parser = ParserCreate()\n        parser.StartElementHandler = p.handleBeginElement\n        parser.EndElementHandler = p.handleEndElement\n        parser.CharacterDataHandler = p.handleData\n        # We also need to monkey patch this to allow for other dict_types, hex int support\n        # proper line output for data errors, and for unicode string decoding\n        def begin_dict(attrs):\n            d = dict_type()\n            p.addObject(d)\n            p.stack.append(d)\n        def end_integer():\n            d = p.getData()\n            value = int(d,16) if d.lower().startswith(\"0x\") else int(d)\n            if -1 << 63 <= value < 1 << 64:\n                p.addObject(value)\n            else:\n                raise OverflowError(\"Integer overflow at line {}\".format(parser.CurrentLineNumber))\n        def end_data():\n            try:\n                p.addObject(plistlib.Data.fromBase64(p.getData()))\n            except Exception as e:\n                raise Exception(\"Data error at line {}: {}\".format(parser.CurrentLineNumber,e))\n        def end_string():\n            d = p.getData()\n            if isinstance(d,unicode):\n                d = d.encode(\"utf-8\")\n            p.addObject(d)\n        p.begin_dict = begin_dict\n        p.end_integer = end_integer\n        p.end_data = end_data\n        p.end_string = end_string\n        if isinstance(fp, unicode):\n            # Encode unicode -> string; use utf-8 for safety\n            fp = fp.encode(\"utf-8\")\n        if isinstance(fp, basestring):\n            # It's a string - let's wrap it up\n            fp = StringIO(fp)\n        # Parse it\n        parser.ParseFile(fp)\n        return p.root\n\ndef loads(value, fmt=None, use_builtin_types=None, dict_type=dict):\n    if _check_py3() and isinstance(value, basestring):\n        # If it's a string - encode it\n        value = value.encode()\n    try:\n        return load(BytesIO(value),fmt=fmt,use_builtin_types=use_builtin_types,dict_type=dict_type)\n    except:\n        # Python 3.9 removed use_builtin_types\n        return load(BytesIO(value),fmt=fmt,dict_type=dict_type)\n\ndef dump(value, fp, fmt=FMT_XML, sort_keys=True, skipkeys=False):\n    if fmt == FMT_BINARY:\n        # Assume binary at this point\n        writer = _BinaryPlistWriter(fp, sort_keys=sort_keys, skipkeys=skipkeys)\n        writer.write(value)\n    elif fmt == FMT_XML:\n        if _check_py3():\n            plistlib.dump(value, fp, fmt=fmt, sort_keys=sort_keys, skipkeys=skipkeys)\n        else:\n            # We need to monkey patch a bunch here too in order to avoid auto-sorting\n            # of keys\n            writer = plistlib.PlistWriter(fp)\n            def writeDict(d):\n                if d:\n                    writer.beginElement(\"dict\")\n                    items = sorted(d.items()) if sort_keys else d.items()\n                    for key, value in items:\n                        if not isinstance(key, basestring):\n                            if skipkeys:\n                                continue\n                            raise TypeError(\"keys must be strings\")\n                        writer.simpleElement(\"key\", key)\n                        writer.writeValue(value)\n                    writer.endElement(\"dict\")\n                else:\n                    writer.simpleElement(\"dict\")\n            writer.writeDict = writeDict\n            writer.writeln(\"<plist version=\\\"1.0\\\">\")\n            writer.writeValue(value)\n            writer.writeln(\"</plist>\")\n    else:\n        # Not a proper format\n        raise ValueError(\"Unsupported format: {}\".format(fmt))\n    \ndef dumps(value, fmt=FMT_XML, skipkeys=False, sort_keys=True):\n    # We avoid using writePlistToString() as that uses\n    # cStringIO and fails when Unicode strings are detected\n    f = BytesIO() if _check_py3() else StringIO()\n    dump(value, f, fmt=fmt, skipkeys=skipkeys, sort_keys=sort_keys)\n    value = f.getvalue()\n    if _check_py3():\n        value = value.decode(\"utf-8\")\n    return value\n\n###                        ###\n# Binary Plist Stuff For Py2 #\n###                        ###\n\n# From the python 3 plistlib.py source:  https://github.com/python/cpython/blob/3.11/Lib/plistlib.py\n# Tweaked to function on both Python 2 and 3\n\nclass UID:\n    def __init__(self, data):\n        if not isinstance(data, int):\n            raise TypeError(\"data must be an int\")\n        # It seems Apple only uses 32-bit unsigned ints for UIDs. Although the comment in\n        # CoreFoundation's CFBinaryPList.c detailing the binary plist format theoretically\n        # allows for 64-bit UIDs, most functions in the same file use 32-bit unsigned ints,\n        # with the sole function hinting at 64-bits appearing to be a leftover from copying\n        # and pasting integer handling code internally, and this code has not changed since\n        # it was added. (In addition, code in CFPropertyList.c to handle CF$UID also uses a\n        # 32-bit unsigned int.)\n        #\n        # if data >= 1 << 64:\n        #    raise ValueError(\"UIDs cannot be >= 2**64\")\n        if data >= 1 << 32:\n            raise ValueError(\"UIDs cannot be >= 2**32 (4294967296)\")\n        if data < 0:\n            raise ValueError(\"UIDs must be positive\")\n        self.data = data\n\n    def __index__(self):\n        return self.data\n\n    def __repr__(self):\n        return \"%s(%s)\" % (self.__class__.__name__, repr(self.data))\n\n    def __reduce__(self):\n        return self.__class__, (self.data,)\n\n    def __eq__(self, other):\n        if not isinstance(other, UID):\n            return NotImplemented\n        return self.data == other.data\n\n    def __hash__(self):\n        return hash(self.data)\n\nclass InvalidFileException (ValueError):\n    def __init__(self, message=\"Invalid file\"):\n        ValueError.__init__(self, message)\n\n_BINARY_FORMAT = {1: 'B', 2: 'H', 4: 'L', 8: 'Q'}\n\n_undefined = object()\n\nclass _BinaryPlistParser:\n    \"\"\"\n    Read or write a binary plist file, following the description of the binary\n    format.  Raise InvalidFileException in case of error, otherwise return the\n    root object.\n    see also: http://opensource.apple.com/source/CF/CF-744.18/CFBinaryPList.c\n    \"\"\"\n    def __init__(self, use_builtin_types, dict_type):\n        self._use_builtin_types = use_builtin_types\n        self._dict_type = dict_type\n\n    def parse(self, fp):\n        try:\n            # The basic file format:\n            # HEADER\n            # object...\n            # refid->offset...\n            # TRAILER\n            self._fp = fp\n            self._fp.seek(-32, os.SEEK_END)\n            trailer = self._fp.read(32)\n            if len(trailer) != 32:\n                raise InvalidFileException()\n            (\n                offset_size, self._ref_size, num_objects, top_object,\n                offset_table_offset\n            ) = struct.unpack('>6xBBQQQ', trailer)\n            self._fp.seek(offset_table_offset)\n            self._object_offsets = self._read_ints(num_objects, offset_size)\n            self._objects = [_undefined] * num_objects\n            return self._read_object(top_object)\n\n        except (OSError, IndexError, struct.error, OverflowError,\n                UnicodeDecodeError):\n            raise InvalidFileException()\n\n    def _get_size(self, tokenL):\n        \"\"\" return the size of the next object.\"\"\"\n        if tokenL == 0xF:\n            m = self._fp.read(1)[0]\n            if not _check_py3():\n                m = ord(m)\n            m = m & 0x3\n            s = 1 << m\n            f = '>' + _BINARY_FORMAT[s]\n            return struct.unpack(f, self._fp.read(s))[0]\n\n        return tokenL\n\n    def _read_ints(self, n, size):\n        data = self._fp.read(size * n)\n        if size in _BINARY_FORMAT:\n            return struct.unpack('>' + _BINARY_FORMAT[size] * n, data)\n        else:\n            if not size or len(data) != size * n:\n                raise InvalidFileException()\n            return tuple(int(binascii.hexlify(data[i: i + size]),16)\n                         for i in range(0, size * n, size))\n            '''return tuple(int.from_bytes(data[i: i + size], 'big')\n                         for i in range(0, size * n, size))'''\n\n    def _read_refs(self, n):\n        return self._read_ints(n, self._ref_size)\n\n    def _read_object(self, ref):\n        \"\"\"\n        read the object by reference.\n        May recursively read sub-objects (content of an array/dict/set)\n        \"\"\"\n        result = self._objects[ref]\n        if result is not _undefined:\n            return result\n\n        offset = self._object_offsets[ref]\n        self._fp.seek(offset)\n        token = self._fp.read(1)[0]\n        if not _check_py3():\n            token = ord(token)\n        tokenH, tokenL = token & 0xF0, token & 0x0F\n\n        if token == 0x00: # \\x00 or 0x00\n            result = None\n\n        elif token == 0x08: # \\x08 or 0x08\n            result = False\n\n        elif token == 0x09: # \\x09 or 0x09\n            result = True\n\n        # The referenced source code also mentions URL (0x0c, 0x0d) and\n        # UUID (0x0e), but neither can be generated using the Cocoa libraries.\n\n        elif token == 0x0f: # \\x0f or 0x0f\n            result = b''\n\n        elif tokenH == 0x10:  # int\n            result = int(binascii.hexlify(self._fp.read(1 << tokenL)),16)\n            if tokenL >= 3: # Signed - adjust\n                result = result-(result & 1 << 2**tokenL*8-1)*2\n\n        elif token == 0x22: # real\n            result = struct.unpack('>f', self._fp.read(4))[0]\n\n        elif token == 0x23: # real\n            result = struct.unpack('>d', self._fp.read(8))[0]\n\n        elif token == 0x33:  # date\n            f = struct.unpack('>d', self._fp.read(8))[0]\n            # timestamp 0 of binary plists corresponds to 1/1/2001\n            # (year of Mac OS X 10.0), instead of 1/1/1970.\n            result = (datetime.datetime(2001, 1, 1) +\n                      datetime.timedelta(seconds=f))\n\n        elif tokenH == 0x40:  # data\n            s = self._get_size(tokenL)\n            if self._use_builtin_types or not hasattr(plistlib, \"Data\"):\n                result = self._fp.read(s)\n            else:\n                result = plistlib.Data(self._fp.read(s))\n\n        elif tokenH == 0x50:  # ascii string\n            s = self._get_size(tokenL)\n            result =  self._fp.read(s).decode('ascii')\n            result = result\n\n        elif tokenH == 0x60:  # unicode string\n            s = self._get_size(tokenL)\n            result = self._fp.read(s * 2).decode('utf-16be')\n\n        elif tokenH == 0x80:  # UID\n            # used by Key-Archiver plist files\n            result = UID(int(binascii.hexlify(self._fp.read(1 + tokenL)),16))\n\n        elif tokenH == 0xA0:  # array\n            s = self._get_size(tokenL)\n            obj_refs = self._read_refs(s)\n            result = []\n            self._objects[ref] = result\n            result.extend(self._read_object(x) for x in obj_refs)\n\n        # tokenH == 0xB0 is documented as 'ordset', but is not actually\n        # implemented in the Apple reference code.\n\n        # tokenH == 0xC0 is documented as 'set', but sets cannot be used in\n        # plists.\n\n        elif tokenH == 0xD0:  # dict\n            s = self._get_size(tokenL)\n            key_refs = self._read_refs(s)\n            obj_refs = self._read_refs(s)\n            result = self._dict_type()\n            self._objects[ref] = result\n            for k, o in zip(key_refs, obj_refs):\n                key = self._read_object(k)\n                if hasattr(plistlib, \"Data\") and isinstance(key, plistlib.Data):\n                    key = key.data\n                result[key] = self._read_object(o)\n\n        else:\n            raise InvalidFileException()\n\n        self._objects[ref] = result\n        return result\n\ndef _count_to_size(count):\n    if count < 1 << 8:\n        return 1\n\n    elif count < 1 << 16:\n        return 2\n\n    elif count < 1 << 32:\n        return 4\n\n    else:\n        return 8\n\n_scalars = (str, int, float, datetime.datetime, bytes)\n\nclass _BinaryPlistWriter (object):\n    def __init__(self, fp, sort_keys, skipkeys):\n        self._fp = fp\n        self._sort_keys = sort_keys\n        self._skipkeys = skipkeys\n\n    def write(self, value):\n\n        # Flattened object list:\n        self._objlist = []\n\n        # Mappings from object->objectid\n        # First dict has (type(object), object) as the key,\n        # second dict is used when object is not hashable and\n        # has id(object) as the key.\n        self._objtable = {}\n        self._objidtable = {}\n\n        # Create list of all objects in the plist\n        self._flatten(value)\n\n        # Size of object references in serialized containers\n        # depends on the number of objects in the plist.\n        num_objects = len(self._objlist)\n        self._object_offsets = [0]*num_objects\n        self._ref_size = _count_to_size(num_objects)\n\n        self._ref_format = _BINARY_FORMAT[self._ref_size]\n\n        # Write file header\n        self._fp.write(b'bplist00')\n\n        # Write object list\n        for obj in self._objlist:\n            self._write_object(obj)\n\n        # Write refnum->object offset table\n        top_object = self._getrefnum(value)\n        offset_table_offset = self._fp.tell()\n        offset_size = _count_to_size(offset_table_offset)\n        offset_format = '>' + _BINARY_FORMAT[offset_size] * num_objects\n        self._fp.write(struct.pack(offset_format, *self._object_offsets))\n\n        # Write trailer\n        sort_version = 0\n        trailer = (\n            sort_version, offset_size, self._ref_size, num_objects,\n            top_object, offset_table_offset\n        )\n        self._fp.write(struct.pack('>5xBBBQQQ', *trailer))\n\n    def _flatten(self, value):\n        # First check if the object is in the object table, not used for\n        # containers to ensure that two subcontainers with the same contents\n        # will be serialized as distinct values.\n        if isinstance(value, _scalars):\n            if (type(value), value) in self._objtable:\n                return\n\n        elif hasattr(plistlib, \"Data\") and isinstance(value, plistlib.Data):\n            if (type(value.data), value.data) in self._objtable:\n                return\n\n        elif id(value) in self._objidtable:\n            return\n\n        # Add to objectreference map\n        refnum = len(self._objlist)\n        self._objlist.append(value)\n        if isinstance(value, _scalars):\n            self._objtable[(type(value), value)] = refnum\n        elif hasattr(plistlib, \"Data\") and isinstance(value, plistlib.Data):\n            self._objtable[(type(value.data), value.data)] = refnum\n        else:\n            self._objidtable[id(value)] = refnum\n\n        # And finally recurse into containers\n        if isinstance(value, dict):\n            keys = []\n            values = []\n            items = value.items()\n            if self._sort_keys:\n                items = sorted(items)\n\n            for k, v in items:\n                if not isinstance(k, basestring):\n                    if self._skipkeys:\n                        continue\n                    raise TypeError(\"keys must be strings\")\n                keys.append(k)\n                values.append(v)\n\n            for o in itertools.chain(keys, values):\n                self._flatten(o)\n\n        elif isinstance(value, (list, tuple)):\n            for o in value:\n                self._flatten(o)\n\n    def _getrefnum(self, value):\n        if isinstance(value, _scalars):\n            return self._objtable[(type(value), value)]\n        elif hasattr(plistlib, \"Data\") and isinstance(value, plistlib.Data):\n            return self._objtable[(type(value.data), value.data)]\n        else:\n            return self._objidtable[id(value)]\n\n    def _write_size(self, token, size):\n        if size < 15:\n            self._fp.write(struct.pack('>B', token | size))\n\n        elif size < 1 << 8:\n            self._fp.write(struct.pack('>BBB', token | 0xF, 0x10, size))\n\n        elif size < 1 << 16:\n            self._fp.write(struct.pack('>BBH', token | 0xF, 0x11, size))\n\n        elif size < 1 << 32:\n            self._fp.write(struct.pack('>BBL', token | 0xF, 0x12, size))\n\n        else:\n            self._fp.write(struct.pack('>BBQ', token | 0xF, 0x13, size))\n\n    def _write_object(self, value):\n        ref = self._getrefnum(value)\n        self._object_offsets[ref] = self._fp.tell()\n        if value is None:\n            self._fp.write(b'\\x00')\n\n        elif value is False:\n            self._fp.write(b'\\x08')\n\n        elif value is True:\n            self._fp.write(b'\\x09')\n\n        elif isinstance(value, int):\n            if value < 0:\n                try:\n                    self._fp.write(struct.pack('>Bq', 0x13, value))\n                except struct.error:\n                    raise OverflowError(value) # from None\n            elif value < 1 << 8:\n                self._fp.write(struct.pack('>BB', 0x10, value))\n            elif value < 1 << 16:\n                self._fp.write(struct.pack('>BH', 0x11, value))\n            elif value < 1 << 32:\n                self._fp.write(struct.pack('>BL', 0x12, value))\n            elif value < 1 << 63:\n                self._fp.write(struct.pack('>BQ', 0x13, value))\n            elif value < 1 << 64:\n                self._fp.write(binascii.unhexlify(\"14\"+hex(value)[2:].rstrip(\"L\").rjust(32,\"0\")))\n            else:\n                raise OverflowError(value)\n\n        elif isinstance(value, float):\n            self._fp.write(struct.pack('>Bd', 0x23, value))\n\n        elif isinstance(value, datetime.datetime):\n            f = (value - datetime.datetime(2001, 1, 1)).total_seconds()\n            self._fp.write(struct.pack('>Bd', 0x33, f))\n\n        elif (_check_py3() and isinstance(value, (bytes, bytearray))) or (hasattr(plistlib, \"Data\") and isinstance(value, plistlib.Data)):\n            if not isinstance(value, (bytes, bytearray)):\n                value = value.data # Unpack it\n            self._write_size(0x40, len(value))\n            self._fp.write(value)\n\n        elif isinstance(value, basestring):\n            try:\n                t = value.encode('ascii')\n                self._write_size(0x50, len(value))\n            except UnicodeEncodeError:\n                t = value.encode('utf-16be')\n                self._write_size(0x60, len(t) // 2)\n            self._fp.write(t)\n\n        elif isinstance(value, UID) or (hasattr(plistlib,\"UID\") and isinstance(value, plistlib.UID)):\n            if value.data < 0:\n                raise ValueError(\"UIDs must be positive\")\n            elif value.data < 1 << 8:\n                self._fp.write(struct.pack('>BB', 0x80, value))\n            elif value.data < 1 << 16:\n                self._fp.write(struct.pack('>BH', 0x81, value))\n            elif value.data < 1 << 32:\n                self._fp.write(struct.pack('>BL', 0x83, value))\n            # elif value.data < 1 << 64:\n            #    self._fp.write(struct.pack('>BQ', 0x87, value))\n            else:\n                raise OverflowError(value)\n\n        elif isinstance(value, (list, tuple)):\n            refs = [self._getrefnum(o) for o in value]\n            s = len(refs)\n            self._write_size(0xA0, s)\n            self._fp.write(struct.pack('>' + self._ref_format * s, *refs))\n\n        elif isinstance(value, dict):\n            keyRefs, valRefs = [], []\n\n            if self._sort_keys:\n                rootItems = sorted(value.items())\n            else:\n                rootItems = value.items()\n\n            for k, v in rootItems:\n                if not isinstance(k, basestring):\n                    if self._skipkeys:\n                        continue\n                    raise TypeError(\"keys must be strings\")\n                keyRefs.append(self._getrefnum(k))\n                valRefs.append(self._getrefnum(v))\n\n            s = len(keyRefs)\n            self._write_size(0xD0, s)\n            self._fp.write(struct.pack('>' + self._ref_format * s, *keyRefs))\n            self._fp.write(struct.pack('>' + self._ref_format * s, *valRefs))\n\n        else:\n            raise TypeError(value)\n"
  },
  {
    "path": "Scripts/run.py",
    "content": "import sys, subprocess, time, threading, shlex\r\ntry:\r\n    from Queue import Queue, Empty\r\nexcept:\r\n    from queue import Queue, Empty\r\n\r\nON_POSIX = 'posix' in sys.builtin_module_names\r\n\r\nclass Run:\r\n\r\n    def __init__(self):\r\n        return\r\n\r\n    def _read_output(self, pipe, q):\r\n        try:\r\n            for line in iter(lambda: pipe.read(1), b''):\r\n                q.put(line)\r\n        except ValueError:\r\n            pass\r\n        pipe.close()\r\n\r\n    def _create_thread(self, output):\r\n        # Creates a new queue and thread object to watch based on the output pipe sent\r\n        q = Queue()\r\n        t = threading.Thread(target=self._read_output, args=(output, q))\r\n        t.daemon = True\r\n        return (q,t)\r\n\r\n    def _stream_output(self, comm, shell = False):\r\n        output = error = \"\"\r\n        p = None\r\n        try:\r\n            if shell and type(comm) is list:\r\n                comm = \" \".join(shlex.quote(x) for x in comm)\r\n            if not shell and type(comm) is str:\r\n                comm = shlex.split(comm)\r\n            p = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0, universal_newlines=True, close_fds=ON_POSIX)\r\n            # Setup the stdout thread/queue\r\n            q,t   = self._create_thread(p.stdout)\r\n            qe,te = self._create_thread(p.stderr)\r\n            # Start both threads\r\n            t.start()\r\n            te.start()\r\n\r\n            while True:\r\n                c = z = \"\"\r\n                try: c = q.get_nowait()\r\n                except Empty: pass\r\n                else:\r\n                    sys.stdout.write(c)\r\n                    output += c\r\n                    sys.stdout.flush()\r\n                try: z = qe.get_nowait()\r\n                except Empty: pass\r\n                else:\r\n                    sys.stderr.write(z)\r\n                    error += z\r\n                    sys.stderr.flush()\r\n                if not c==z==\"\": continue # Keep going until empty\r\n                # No output - see if still running\r\n                p.poll()\r\n                if p.returncode != None:\r\n                    # Subprocess ended\r\n                    break\r\n                # No output, but subprocess still running - stall for 20ms\r\n                time.sleep(0.02)\r\n\r\n            o, e = p.communicate()\r\n            return (output+o, error+e, p.returncode)\r\n        except:\r\n            if p:\r\n                try: o, e = p.communicate()\r\n                except: o = e = \"\"\r\n                return (output+o, error+e, p.returncode)\r\n            return (\"\", \"Command not found!\", 1)\r\n\r\n    def _decode(self, value, encoding=\"utf-8\", errors=\"ignore\"):\r\n        # Helper method to only decode if bytes type\r\n        if sys.version_info >= (3,0) and isinstance(value, bytes):\r\n            return value.decode(encoding,errors)\r\n        return value\r\n\r\n    def _run_command(self, comm, shell = False):\r\n        c = None\r\n        try:\r\n            if shell and type(comm) is list:\r\n                comm = \" \".join(shlex.quote(x) for x in comm)\r\n            if not shell and type(comm) is str:\r\n                comm = shlex.split(comm)\r\n            p = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\r\n            c = p.communicate()\r\n        except:\r\n            if c == None:\r\n                return (\"\", \"Command not found!\", 1)\r\n        return (self._decode(c[0]), self._decode(c[1]), p.returncode)\r\n\r\n    def run(self, command_list, leave_on_fail = False):\r\n        # Command list should be an array of dicts\r\n        if type(command_list) is dict:\r\n            # We only have one command\r\n            command_list = [command_list]\r\n        output_list = []\r\n        for comm in command_list:\r\n            args   = comm.get(\"args\",   [])\r\n            shell  = comm.get(\"shell\",  False)\r\n            stream = comm.get(\"stream\", False)\r\n            sudo   = comm.get(\"sudo\",   False)\r\n            stdout = comm.get(\"stdout\", False)\r\n            stderr = comm.get(\"stderr\", False)\r\n            mess   = comm.get(\"message\", None)\r\n            show   = comm.get(\"show\",   False)\r\n            \r\n            if not mess == None:\r\n                print(mess)\r\n\r\n            if not len(args):\r\n                # nothing to process\r\n                continue\r\n            if sudo:\r\n                # Check if we have sudo\r\n                out = self._run_command([\"which\", \"sudo\"])\r\n                if \"sudo\" in out[0]:\r\n                    # Can sudo\r\n                    if type(args) is list:\r\n                        args.insert(0, out[0].replace(\"\\n\", \"\")) # add to start of list\r\n                    elif type(args) is str:\r\n                        args = out[0].replace(\"\\n\", \"\") + \" \" + args # add to start of string\r\n            \r\n            if show:\r\n                print(\" \".join(args))\r\n\r\n            if stream:\r\n                # Stream it!\r\n                out = self._stream_output(args, shell)\r\n            else:\r\n                # Just run and gather output\r\n                out = self._run_command(args, shell)\r\n                if stdout and len(out[0]):\r\n                    print(out[0])\r\n                if stderr and len(out[1]):\r\n                    print(out[1])\r\n            # Append output\r\n            output_list.append(out)\r\n            # Check for errors\r\n            if leave_on_fail and out[2] != 0:\r\n                # Got an error - leave\r\n                break\r\n        if len(output_list) == 1:\r\n            # We only ran one command - just return that output\r\n            return output_list[0]\r\n        return output_list\r\n"
  },
  {
    "path": "Scripts/utils.py",
    "content": "import sys, os, time, re, json, datetime, ctypes, subprocess\n\nif os.name == \"nt\":\n    # Windows\n    import msvcrt\nelse:\n    # Not Windows \\o/\n    import select\n\nclass Utils:\n\n    def __init__(self, name = \"Python Script\", interactive = True):\n        self.name = name\n        self.interactive = interactive\n        # Init our colors before we need to print anything\n        cwd = os.getcwd()\n        os.chdir(os.path.dirname(os.path.realpath(__file__)))\n        if os.path.exists(\"colors.json\"):\n            self.colors_dict = json.load(open(\"colors.json\"))\n        else:\n            self.colors_dict = {}\n        os.chdir(cwd)\n\n    def check_admin(self):\n        # Returns whether or not we're admin\n        try:\n            is_admin = os.getuid() == 0\n        except AttributeError:\n            is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0\n        return is_admin\n\n    def elevate(self, file):\n        # Runs the passed file as admin\n        if self.check_admin():\n            return\n        if os.name == \"nt\":\n            ctypes.windll.shell32.ShellExecuteW(None, \"runas\", '\"{}\"'.format(sys.executable), '\"{}\"'.format(file), None, 1)\n        else:\n            try:\n                p = subprocess.Popen([\"which\", \"sudo\"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n                c = p.communicate()[0].decode(\"utf-8\", \"ignore\").replace(\"\\n\", \"\")\n                os.execv(c, [ sys.executable, 'python'] + sys.argv)\n            except:\n                exit(1)\n                \n    def compare_versions(self, vers1, vers2, **kwargs):\n        # Helper method to compare ##.## strings\n        #\n        # vers1 < vers2 = True\n        # vers1 = vers2 = None\n        # vers1 > vers2 = False\n        \n        # Sanitize the pads\n        pad = str(kwargs.get(\"pad\", \"\"))\n        sep = str(kwargs.get(\"separator\", \".\"))\n\n        ignore_case = kwargs.get(\"ignore_case\", True)\n        \n        # Cast as strings\n        vers1 = str(vers1)\n        vers2 = str(vers2)\n        \n        if ignore_case:\n            vers1 = vers1.lower()\n            vers2 = vers2.lower()\n\n        # Split and pad lists\n        v1_parts, v2_parts = self.pad_length(vers1.split(sep), vers2.split(sep))\n        \n        # Iterate and compare\n        for i in range(len(v1_parts)):\n            # Remove non-numeric\n            v1 = ''.join(c.lower() for c in v1_parts[i] if c.isalnum())\n            v2 = ''.join(c.lower() for c in v2_parts[i] if c.isalnum())\n            # Equalize the lengths\n            v1, v2 = self.pad_length(v1, v2)\n            # Compare\n            if str(v1) < str(v2):\n                return True\n            elif str(v1) > str(v2):\n                return False\n        # Never differed - return None, must be equal\n        return None\n\n    def pad_length(self, var1, var2, pad = \"0\"):\n        # Pads the vars on the left side to make them equal length\n        pad = \"0\" if len(str(pad)) < 1 else str(pad)[0]\n        if not type(var1) == type(var2):\n            # Type mismatch! Just return what we got\n            return (var1, var2)\n        if len(var1) < len(var2):\n            if type(var1) is list:\n                var1.extend([str(pad) for x in range(len(var2) - len(var1))])\n            else:\n                var1 = \"{}{}\".format((pad*(len(var2)-len(var1))), var1)\n        elif len(var2) < len(var1):\n            if type(var2) is list:\n                var2.extend([str(pad) for x in range(len(var1) - len(var2))])\n            else:\n                var2 = \"{}{}\".format((pad*(len(var1)-len(var2))), var2)\n        return (var1, var2)\n        \n    def check_path(self, path):\n        # Let's loop until we either get a working path, or no changes\n        test_path = path\n        last_path = None\n        while True:\n            # Bail if we've looped at least once and the path didn't change\n            if last_path != None and last_path == test_path: return None\n            last_path = test_path\n            # Check if we stripped everything out\n            if not len(test_path): return None\n            # Check if we have a valid path\n            if os.path.exists(test_path):\n                return os.path.abspath(test_path)\n            # Check for quotes\n            if test_path[0] == test_path[-1] and test_path[0] in ('\"',\"'\"):\n                test_path = test_path[1:-1]\n                continue\n            # Check for a tilde and expand if needed\n            if test_path[0] == \"~\":\n                tilde_expanded = os.path.expanduser(test_path)\n                if tilde_expanded != test_path:\n                    # Got a change\n                    test_path = tilde_expanded\n                    continue\n            # Let's check for spaces - strip from the left first, then the right\n            if test_path[0] in (\" \",\"\\t\"):\n                test_path = test_path[1:]\n                continue\n            if test_path[-1] in (\" \",\"\\t\"):\n                test_path = test_path[:-1]\n                continue\n            # Maybe we have escapes to handle?\n            test_path = \"\\\\\".join([x.replace(\"\\\\\", \"\") for x in test_path.split(\"\\\\\\\\\")])\n\n    def grab(self, prompt, **kwargs):\n        # Takes a prompt, a default, and a timeout and shows it with that timeout\n        # returning the result\n        timeout = kwargs.get(\"timeout\",0)\n        default = kwargs.get(\"default\",\"\")\n        if not self.interactive:\n            return default\n        # If we don't have a timeout - then skip the timed sections\n        if timeout <= 0:\n            try:\n                if sys.version_info >= (3, 0):\n                    return input(prompt)\n                else:\n                    return str(raw_input(prompt))\n            except EOFError:\n                return default\n        # Write our prompt\n        sys.stdout.write(prompt)\n        sys.stdout.flush()\n        if os.name == \"nt\":\n            start_time = time.time()\n            i = ''\n            while True:\n                if msvcrt.kbhit():\n                    c = msvcrt.getche()\n                    if ord(c) == 13: # enter_key\n                        break\n                    elif ord(c) >= 32: # space_char\n                        i += c.decode() if sys.version_info >= (3,0) and isinstance(c,bytes) else c\n                else:\n                    time.sleep(0.02) # Delay for 20ms to prevent CPU workload\n                if len(i) == 0 and (time.time() - start_time) > timeout:\n                    break\n        else:\n            i, o, e = select.select( [sys.stdin], [], [], timeout )\n            if i:\n                i = sys.stdin.readline().strip()\n        print('')  # needed to move to next line\n        if len(i) > 0:\n            return i\n        else:\n            return default\n\n    def cls(self):\n        if not self.interactive:\n            return\n        if os.name == \"nt\":\n            os.system(\"cls\")\n        elif os.environ.get(\"TERM\"):\n            os.system(\"clear\")\n\n    def cprint(self, message, **kwargs):\n        strip_colors = kwargs.get(\"strip_colors\", False)\n        if os.name == \"nt\" or not self.interactive:\n            strip_colors = True\n        reset = u\"\\u001b[0m\"\n        # Requires sys import\n        for c in self.colors:\n            if strip_colors:\n                message = message.replace(c[\"find\"], \"\")\n            else:\n                message = message.replace(c[\"find\"], c[\"replace\"])\n        if strip_colors:\n            return message\n        sys.stdout.write(message)\n        print(reset)\n\n    # Needs work to resize the string if color chars exist\n    '''# Header drawing method\n    def head(self, text = None, width = 55):\n        if text == None:\n            text = self.name\n        self.cls()\n        print(\"  {}\".format(\"#\"*width))\n        len_text = self.cprint(text, strip_colors=True)\n        mid_len = int(round(width/2-len(len_text)/2)-2)\n        middle = \" #{}{}{}#\".format(\" \"*mid_len, len_text, \" \"*((width - mid_len - len(len_text))-2))\n        if len(middle) > width+1:\n            # Get the difference\n            di = len(middle) - width\n            # Add the padding for the ...#\n            di += 3\n            # Trim the string\n            middle = middle[:-di]\n            newlen = len(middle)\n            middle += \"...#\"\n            find_list = [ c[\"find\"] for c in self.colors ]\n\n            # Translate colored string to len\n        middle = middle.replace(len_text, text + self.rt_color) # always reset just in case\n        self.cprint(middle)\n        print(\"#\"*width)'''\n\n    # Header drawing method\n    def head(self, text = None, width = 55):\n        if not self.interactive:\n            sys.stderr.write(str(text)+\"\\n\")\n            sys.stderr.flush()\n            return\n        if text is None:\n            text = self.name\n        self.cls()\n        print(\"  {}\".format(\"#\"*width))\n        mid_len = int(round(width/2-len(text)/2)-2)\n        middle = \" #{}{}{}#\".format(\" \"*mid_len, text, \" \"*((width - mid_len - len(text))-2))\n        if len(middle) > width+1:\n            # Get the difference\n            di = len(middle) - width\n            # Add the padding for the ...#\n            di += 3\n            # Trim the string\n            middle = middle[:-di] + \"...#\"\n        print(middle)\n        print(\"#\"*width)\n        print(\"\")\n\n    def info(self, text):\n        if self.interactive:\n            print(text)\n        else:\n            sys.stderr.write(str(text)+\"\\n\")\n            sys.stderr.flush()\n\n    def resize(self, width, height):\n        print('\\033[8;{};{}t'.format(height, width))\n\n    def custom_quit(self):\n        self.head()\n        print(\"by CorpNewt\\n\")\n        print(\"Thanks for testing it out, for bugs/comments/complaints\")\n        print(\"send me a message on Reddit, or check out my GitHub:\\n\")\n        print(\"www.reddit.com/u/corpnewt\")\n        print(\"www.github.com/corpnewt\\n\")\n        # Get the time and wish them a good morning, afternoon, evening, and night\n        hr = datetime.datetime.now().time().hour\n        if hr > 3 and hr < 12:\n            print(\"Have a nice morning!\\n\\n\")\n        elif hr >= 12 and hr < 17:\n            print(\"Have a nice afternoon!\\n\\n\")\n        elif hr >= 17 and hr < 21:\n            print(\"Have a nice evening!\\n\\n\")\n        else:\n            print(\"Have a nice night!\\n\\n\")\n        exit(0)\n"
  },
  {
    "path": "gibMacOS.bat",
    "content": "@echo off\r\nREM Get our local path and args before delayed expansion - allows % and !\r\nset \"thisDir=%~dp0\"\r\nset \"args=%*\"\r\n\r\nsetlocal enableDelayedExpansion\r\nREM Setup initial vars\r\nset \"script_name=\"\r\nset /a tried=0\r\nset \"toask=yes\"\r\nset \"pause_on_error=yes\"\r\nset \"py2v=\"\r\nset \"py2path=\"\r\nset \"py3v=\"\r\nset \"py3path=\"\r\nset \"pypath=\"\r\nset \"targetpy=3\"\r\n\r\nREM use_py3:\r\nREM   TRUE  = Use if found, use py2 otherwise\r\nREM   FALSE = Use py2\r\nREM   FORCE = Use py3\r\nset \"use_py3=TRUE\"\r\n\r\nREM We'll parse if the first argument passed is\r\nREM --install-python and if so, we'll just install\r\nREM Can optionally take a version number as the\r\nREM second arg - i.e. --install-python 3.13.1\r\nset \"just_installing=FALSE\"\r\nset \"user_provided=\"\r\n\r\nREM Get the system32 (or equivalent) path\r\ncall :getsyspath \"syspath\"\r\n\r\nREM Make sure the syspath exists\r\nif \"!syspath!\" == \"\" (\r\n    if exist \"%SYSTEMROOT%\\system32\\cmd.exe\" (\r\n        if exist \"%SYSTEMROOT%\\system32\\reg.exe\" (\r\n            if exist \"%SYSTEMROOT%\\system32\\where.exe\" (\r\n                REM Fall back on the default path if it exists\r\n                set \"ComSpec=%SYSTEMROOT%\\system32\\cmd.exe\"\r\n                set \"syspath=%SYSTEMROOT%\\system32\\\"\r\n            )\r\n        )\r\n    )\r\n    if \"!syspath!\" == \"\" (\r\n        cls\r\n        echo   ###                      ###\r\n        echo  #  Missing Required Files  #\r\n        echo ###                      ###\r\n        echo.\r\n        echo Could not locate cmd.exe, reg.exe, or where.exe\r\n        echo.\r\n        echo Please ensure your ComSpec environment variable is properly configured and\r\n        echo points directly to cmd.exe, then try again.\r\n        echo.\r\n        echo Current CompSpec Value: \"%ComSpec%\"\r\n        echo.\r\n        echo Press [enter] to quit.\r\n        pause > nul\r\n        exit /b 1\r\n    )\r\n)\r\n\r\nif \"%~1\" == \"--install-python\" (\r\n    set \"just_installing=TRUE\"\r\n    set \"user_provided=%~2\"\r\n    goto installpy\r\n)\r\n\r\ngoto checkscript\r\n\r\n:checkscript\r\nREM Check for our script first\r\nset \"looking_for=!script_name!\"\r\nif \"!script_name!\" == \"\" (\r\n    set \"looking_for=%~n0.py or %~n0.command\"\r\n    set \"script_name=%~n0.py\"\r\n    if not exist \"!thisDir!\\!script_name!\" (\r\n        set \"script_name=%~n0.command\"\r\n    )\r\n)\r\nif not exist \"!thisDir!\\!script_name!\" (\r\n    cls\r\n    echo   ###                      ###\r\n    echo  #     Target Not Found     #\r\n    echo ###                      ###\r\n    echo.\r\n    echo Could not find !looking_for!.\r\n    echo Please make sure to run this script from the same directory\r\n    echo as !looking_for!.\r\n    echo.\r\n    echo Press [enter] to quit.\r\n    pause > nul\r\n    exit /b 1\r\n)\r\ngoto checkpy\r\n\r\n:checkpy\r\ncall :updatepath\r\nfor /f \"USEBACKQ tokens=*\" %%x in (`!syspath!where.exe python 2^> nul`) do ( call :checkpyversion \"%%x\" \"py2v\" \"py2path\" \"py3v\" \"py3path\" )\r\nfor /f \"USEBACKQ tokens=*\" %%x in (`!syspath!where.exe python3 2^> nul`) do ( call :checkpyversion \"%%x\" \"py2v\" \"py2path\" \"py3v\" \"py3path\" )\r\nfor /f \"USEBACKQ tokens=*\" %%x in (`!syspath!where.exe py 2^> nul`) do ( call :checkpylauncher \"%%x\" \"py2v\" \"py2path\" \"py3v\" \"py3path\" )\r\nREM Walk our returns to see if we need to install\r\nif /i \"!use_py3!\" == \"FALSE\" (\r\n    set \"targetpy=2\"\r\n    set \"pypath=!py2path!\"\r\n) else if /i \"!use_py3!\" == \"FORCE\" (\r\n    set \"pypath=!py3path!\"\r\n) else if /i \"!use_py3!\" == \"TRUE\" (\r\n    set \"pypath=!py3path!\"\r\n    if \"!pypath!\" == \"\" set \"pypath=!py2path!\"\r\n)\r\nif not \"!pypath!\" == \"\" (\r\n    goto runscript\r\n)\r\nif !tried! lss 1 (\r\n    if /i \"!toask!\"==\"yes\" (\r\n        REM Better ask permission first\r\n        goto askinstall\r\n    ) else (\r\n        goto installpy\r\n    )\r\n) else (\r\n    cls\r\n    echo   ###                      ###\r\n    echo  #     Python Not Found     #\r\n    echo ###                      ###\r\n    echo.\r\n    REM Couldn't install for whatever reason - give the error message\r\n    echo Python is not installed or not found in your PATH var.\r\n    echo Please go to https://www.python.org/downloads/windows/ to\r\n    echo download and install the latest version, then try again.\r\n    echo.\r\n    echo Make sure you check the box labeled:\r\n    echo.\r\n    echo \"Add Python X.X to PATH\"\r\n    echo.\r\n    echo Where X.X is the py version you're installing.\r\n    echo.\r\n    echo Press [enter] to quit.\r\n    pause > nul\r\n    exit /b 1\r\n)\r\ngoto runscript\r\n\r\n:checkpylauncher <path> <py2v> <py2path> <py3v> <py3path>\r\nREM Attempt to check the latest python 2 and 3 versions via the py launcher\r\nfor /f \"USEBACKQ tokens=*\" %%x in (`%~1 -2 -c \"import sys; print(sys.executable)\" 2^> nul`) do ( call :checkpyversion \"%%x\" \"%~2\" \"%~3\" \"%~4\" \"%~5\" )\r\nfor /f \"USEBACKQ tokens=*\" %%x in (`%~1 -3 -c \"import sys; print(sys.executable)\" 2^> nul`) do ( call :checkpyversion \"%%x\" \"%~2\" \"%~3\" \"%~4\" \"%~5\" )\r\ngoto :EOF\r\n\r\n:checkpyversion <path> <py2v> <py2path> <py3v> <py3path>\r\nset \"version=\"&for /f \"tokens=2* USEBACKQ delims= \" %%a in (`\"%~1\" -V 2^>^&1`) do (\r\n    REM Ensure we have a version number\r\n    call :isnumber \"%%a\"\r\n    if not \"!errorlevel!\" == \"0\" goto :EOF\r\n    set \"version=%%a\"\r\n)\r\nif not defined version goto :EOF\r\nif \"!version:~0,1!\" == \"2\" (\r\n    REM Python 2\r\n    call :comparepyversion \"!version!\" \"!%~2!\"\r\n    if \"!errorlevel!\" == \"1\" (\r\n        set \"%~2=!version!\"\r\n        set \"%~3=%~1\"\r\n    )\r\n) else (\r\n    REM Python 3\r\n    call :comparepyversion \"!version!\" \"!%~4!\"\r\n    if \"!errorlevel!\" == \"1\" (\r\n        set \"%~4=!version!\"\r\n        set \"%~5=%~1\"\r\n    )\r\n)\r\ngoto :EOF\r\n\r\n:isnumber <check_value>\r\nset \"var=\"&for /f \"delims=0123456789.\" %%i in (\"%~1\") do set var=%%i\r\nif defined var (exit /b 1)\r\nexit /b 0\r\n\r\n:comparepyversion <version1> <version2> <return>\r\nREM Exits with status 0 if equal, 1 if v1 gtr v2, 2 if v1 lss v2\r\nfor /f \"tokens=1,2,3 delims=.\" %%a in (\"%~1\") do (\r\n    set a1=%%a\r\n    set a2=%%b\r\n    set a3=%%c\r\n)\r\nfor /f \"tokens=1,2,3 delims=.\" %%a in (\"%~2\") do (\r\n    set b1=%%a\r\n    set b2=%%b\r\n    set b3=%%c\r\n)\r\nif not defined a1 set a1=0\r\nif not defined a2 set a2=0\r\nif not defined a3 set a3=0\r\nif not defined b1 set b1=0\r\nif not defined b2 set b2=0\r\nif not defined b3 set b3=0\r\nif %a1% gtr %b1% exit /b 1\r\nif %a1% lss %b1% exit /b 2\r\nif %a2% gtr %b2% exit /b 1\r\nif %a2% lss %b2% exit /b 2\r\nif %a3% gtr %b3% exit /b 1\r\nif %a3% lss %b3% exit /b 2\r\nexit /b 0\r\n\r\n:askinstall\r\ncls\r\necho   ###                      ###\r\necho  #     Python Not Found     #\r\necho ###                      ###\r\necho.\r\necho Python !targetpy! was not found on the system or in the PATH var.\r\necho.\r\nset /p \"menu=Would you like to install it now? [y/n]: \"\r\nif /i \"!menu!\"==\"y\" (\r\n    REM We got the OK - install it\r\n    goto installpy\r\n) else if \"!menu!\"==\"n\" (\r\n    REM No OK here...\r\n    set /a tried=!tried!+1\r\n    goto checkpy\r\n)\r\nREM Incorrect answer - go back\r\ngoto askinstall\r\n\r\n:installpy\r\nREM This will attempt to download and install python\r\nset /a tried=!tried!+1\r\ncls\r\necho   ###                        ###\r\necho  #     Downloading Python     #\r\necho ###                        ###\r\necho.\r\nset \"release=!user_provided!\"\r\nif \"!release!\" == \"\" (\r\n    REM No explicit release set - get the latest from python.org\r\n    echo Gathering latest version...\r\n    powershell -command \"[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12;(new-object System.Net.WebClient).DownloadFile('https://www.python.org/downloads/windows/','%TEMP%\\pyurl.txt')\"\r\n    REM Extract it if it's gzip compressed\r\n    powershell -command \"$infile='%TEMP%\\pyurl.txt';$outfile='%TEMP%\\pyurl.temp';try{$input=New-Object System.IO.FileStream $infile,([IO.FileMode]::Open),([IO.FileAccess]::Read),([IO.FileShare]::Read);$output=New-Object System.IO.FileStream $outfile,([IO.FileMode]::Create),([IO.FileAccess]::Write),([IO.FileShare]::None);$gzipStream=New-Object System.IO.Compression.GzipStream $input,([IO.Compression.CompressionMode]::Decompress);$buffer=New-Object byte[](1024);while($true){$read=$gzipstream.Read($buffer,0,1024);if($read -le 0){break};$output.Write($buffer,0,$read)};$gzipStream.Close();$output.Close();$input.Close();Move-Item -Path $outfile -Destination $infile -Force}catch{}\"\r\n    if not exist \"%TEMP%\\pyurl.txt\" (\r\n        if /i \"!just_installing!\" == \"TRUE\" (\r\n            echo  - Failed to get info\r\n            exit /b 1\r\n        ) else (\r\n            goto checkpy\r\n        )\r\n    )\r\n    pushd \"%TEMP%\"\r\n    :: Version detection code slimmed by LussacZheng (https://github.com/corpnewt/gibMacOS/issues/20)\r\n    for /f \"tokens=9 delims=< \" %%x in ('findstr /i /c:\"Latest Python !targetpy! Release\" pyurl.txt') do ( set \"release=%%x\" )\r\n    popd\r\n    REM Let's delete our txt file now - we no longer need it\r\n    del \"%TEMP%\\pyurl.txt\"\r\n    if \"!release!\" == \"\" (\r\n        if /i \"!just_installing!\" == \"TRUE\" (\r\n            echo  - Failed to get python version\r\n            exit /b 1\r\n        ) else (\r\n            goto checkpy\r\n        )\r\n    )\r\n    echo Located Version:  !release!\r\n) else (\r\n    echo User-Provided Version:  !release!\r\n    REM Update our targetpy to reflect the first number of\r\n    REM our release\r\n    for /f \"tokens=1 delims=.\" %%a in (\"!release!\") do (\r\n        call :isnumber \"%%a\"\r\n        if \"!errorlevel!\" == \"0\" (\r\n            set \"targetpy=%%a\"\r\n        )\r\n    )\r\n)\r\necho Building download url...\r\nREM At this point - we should have the version number.\r\nREM We can build the url like so: \"https://www.python.org/ftp/python/[version]/python-[version]-amd64.exe\"\r\nset \"url=https://www.python.org/ftp/python/!release!/python-!release!-amd64.exe\"\r\nset \"pytype=exe\"\r\nif \"!targetpy!\" == \"2\" (\r\n    set \"url=https://www.python.org/ftp/python/!release!/python-!release!.amd64.msi\"\r\n    set \"pytype=msi\"\r\n)\r\necho  - !url!\r\necho Downloading...\r\nREM Now we download it with our slick powershell command\r\npowershell -command \"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (new-object System.Net.WebClient).DownloadFile('!url!','%TEMP%\\pyinstall.!pytype!')\"\r\nREM If it doesn't exist - we bail\r\nif not exist \"%TEMP%\\pyinstall.!pytype!\" (\r\n    if /i \"!just_installing!\" == \"TRUE\" (\r\n        echo  - Failed to download python installer\r\n        exit /b 1\r\n    ) else (\r\n        goto checkpy\r\n    )\r\n)\r\nREM It should exist at this point - let's run it to install silently\r\necho Running python !pytype! installer...\r\npushd \"%TEMP%\"\r\nif /i \"!pytype!\" == \"exe\" (\r\n    echo  - pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0\r\n    pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0\r\n) else (\r\n    set \"foldername=!release:.=!\"\r\n    echo  - msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR=\"%LocalAppData%\\Programs\\Python\\Python!foldername:~0,2!\"\r\n    msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR=\"%LocalAppData%\\Programs\\Python\\Python!foldername:~0,2!\"\r\n)\r\npopd\r\nset \"py_error=!errorlevel!\"\r\necho Installer finished with status: !py_error!\r\necho Cleaning up...\r\nREM Now we should be able to delete the installer and check for py again\r\ndel \"%TEMP%\\pyinstall.!pytype!\"\r\nREM If it worked, then we should have python in our PATH\r\nREM this does not get updated right away though - let's try\r\nREM manually updating the local PATH var\r\ncall :updatepath\r\nif /i \"!just_installing!\" == \"TRUE\" (\r\n    echo.\r\n    echo Done.\r\n) else (\r\n    goto checkpy\r\n)\r\nexit /b\r\n\r\n:runscript\r\nREM Python found\r\ncls\r\nREM Checks the args gathered at the beginning of the script.\r\nREM Make sure we're not just forwarding empty quotes.\r\nset \"arg_test=!args:\"=!\"\r\nif \"!arg_test!\"==\"\" (\r\n    \"!pypath!\" \"!thisDir!!script_name!\"\r\n) else (\r\n    \"!pypath!\" \"!thisDir!!script_name!\" !args!\r\n)\r\nif /i \"!pause_on_error!\" == \"yes\" (\r\n    if not \"%ERRORLEVEL%\" == \"0\" (\r\n        echo.\r\n        echo Script exited with error code: %ERRORLEVEL%\r\n        echo.\r\n        echo Press [enter] to exit...\r\n        pause > nul\r\n    )\r\n)\r\ngoto :EOF\r\n\r\n:undouble <string_name> <string_value> <character>\r\nREM Helper function to strip doubles of a single character out of a string recursively\r\nset \"string_value=%~2\"\r\n:undouble_continue\r\nset \"check=!string_value:%~3%~3=%~3!\"\r\nif not \"!check!\" == \"!string_value!\" (\r\n    set \"string_value=!check!\"\r\n    goto :undouble_continue\r\n)\r\nset \"%~1=!check!\"\r\ngoto :EOF\r\n\r\n:updatepath\r\nset \"spath=\"\r\nset \"upath=\"\r\nfor /f \"USEBACKQ tokens=2* delims= \" %%i in (`!syspath!reg.exe query \"HKCU\\Environment\" /v \"Path\" 2^> nul`) do ( if not \"%%j\" == \"\" set \"upath=%%j\" )\r\nfor /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\" )\r\nif not \"%spath%\" == \"\" (\r\n    REM We got something in the system path\r\n    set \"PATH=%spath%\"\r\n    if not \"%upath%\" == \"\" (\r\n        REM We also have something in the user path\r\n        set \"PATH=%PATH%;%upath%\"\r\n    )\r\n) else if not \"%upath%\" == \"\" (\r\n    set \"PATH=%upath%\"\r\n)\r\nREM Remove double semicolons from the adjusted PATH\r\ncall :undouble \"PATH\" \"%PATH%\" \";\"\r\ngoto :EOF\r\n\r\n:getsyspath <variable_name>\r\nREM Helper method to return a valid path to cmd.exe, reg.exe, and where.exe by\r\nREM walking the ComSpec var - will also repair it in memory if need be\r\nREM Strip double semi-colons\r\ncall :undouble \"temppath\" \"%ComSpec%\" \";\"\r\n\r\nREM Dirty hack to leverage the \"line feed\" approach - there are some odd side\r\nREM effects with this.  Do not use this variable name in comments near this\r\nREM line - as it seems to behave erradically.\r\n(set LF=^\r\n%=this line is empty=%\r\n)\r\nREM Replace instances of semi-colons with a line feed and wrap\r\nREM in parenthesis to work around some strange batch behavior\r\nset \"testpath=%temppath:;=!LF!%\"\r\n\r\nREM Let's walk each path and test if cmd.exe, reg.exe, and where.exe exist there\r\nset /a found=0\r\nfor /f \"tokens=* delims=\" %%i in (\"!testpath!\") do (\r\n    REM Only continue if we haven't found it yet\r\n    if not \"%%i\" == \"\" (\r\n        if !found! lss 1 (\r\n            set \"checkpath=%%i\"\r\n            REM Remove \"cmd.exe\" from the end if it exists\r\n            if /i \"!checkpath:~-7!\" == \"cmd.exe\" (\r\n                set \"checkpath=!checkpath:~0,-7!\"\r\n            )\r\n            REM Pad the end with a backslash if needed\r\n            if not \"!checkpath:~-1!\" == \"\\\" (\r\n                set \"checkpath=!checkpath!\\\"\r\n            )\r\n            REM Let's see if cmd, reg, and where exist there - and set it if so\r\n            if EXIST \"!checkpath!cmd.exe\" (\r\n                if EXIST \"!checkpath!reg.exe\" (\r\n                    if EXIST \"!checkpath!where.exe\" (\r\n                        set /a found=1\r\n                        set \"ComSpec=!checkpath!cmd.exe\"\r\n                        set \"%~1=!checkpath!\"\r\n                    )\r\n                )\r\n            )\r\n        )\r\n    )\r\n)\r\ngoto :EOF\r\n"
  },
  {
    "path": "gibMacOS.command",
    "content": "#!/usr/bin/env bash\n\n# Get the curent directory, the script name\n# and the script name with \"py\" substituted for the extension.\nargs=( \"$@\" )\ndir=\"$(cd -- \"$(dirname \"$0\")\" >/dev/null 2>&1; pwd -P)\"\nscript=\"${0##*/}\"\ntarget=\"${script%.*}.py\"\n\n# use_py3:\n#   TRUE  = Use if found, use py2 otherwise\n#   FALSE = Use py2\n#   FORCE = Use py3\nuse_py3=\"TRUE\"\n\n# We'll parse if the first argument passed is\n# --install-python and if so, we'll just install\n# Can optionally take a version number as the\n# second arg - i.e. --install-python 3.13.1\njust_installing=\"FALSE\"\n\ntempdir=\"\"\n\ncompare_to_version () {\n    # Compares our OS version to the passed OS version, and\n    # return a 1 if we match the passed compare type, or a 0 if we don't.\n    # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal)\n    # $2 = OS version to compare ours to\n    if [ -z \"$1\" ] || [ -z \"$2\" ]; then\n        # Missing info - bail.\n        return\n    fi\n    local current_os= comp=\n    current_os=\"$(sw_vers -productVersion 2>/dev/null)\"\n    comp=\"$(vercomp \"$current_os\" \"$2\")\"\n    # Check gequal and lequal first\n    if [[ \"$1\" == \"3\" && (\"$comp\" == \"1\" || \"$comp\" == \"0\") ]] || [[ \"$1\" == \"4\" && (\"$comp\" == \"2\" || \"$comp\" == \"0\") ]] || [[ \"$comp\" == \"$1\" ]]; then\n        # Matched\n        echo \"1\"\n    else\n        # No match\n        echo \"0\"\n    fi\n}\n\nset_use_py3_if () {\n    # Auto sets the \"use_py3\" variable based on\n    # conditions passed\n    # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal)\n    # $2 = OS version to compare\n    # $3 = TRUE/FALSE/FORCE in case of match\n    if [ -z \"$1\" ] || [ -z \"$2\" ] || [ -z \"$3\" ]; then\n        # Missing vars - bail with no changes.\n        return\n    fi\n    if [ \"$(compare_to_version \"$1\" \"$2\")\" == \"1\" ]; then\n        use_py3=\"$3\"\n    fi\n}\n\nget_remote_py_version () {\n    local pyurl= py_html= py_vers= py_num=\"3\"\n    pyurl=\"https://www.python.org/downloads/macos/\"\n    py_html=\"$(curl -L $pyurl --compressed 2>&1)\"\n    if [ -z \"$use_py3\" ]; then\n        use_py3=\"TRUE\"\n    fi\n    if [ \"$use_py3\" == \"FALSE\" ]; then\n        py_num=\"2\"\n    fi\n    py_vers=\"$(echo \"$py_html\" | grep -i \"Latest Python $py_num Release\" | awk '{print $8}' | cut -d'<' -f1)\"\n    echo \"$py_vers\"\n}\n\ndownload_py () {\n    local vers=\"$1\" url=\n    clear\n    echo \"  ###                        ###\"\n    echo \" #     Downloading Python     #\"\n    echo \"###                        ###\"\n    echo\n    if [ -z \"$vers\" ]; then\n        echo \"Gathering latest version...\"\n        vers=\"$(get_remote_py_version)\"\n        if [ -z \"$vers\" ]; then\n            if [ \"$just_installing\" == \"TRUE\" ]; then\n                echo \" - Failed to get info!\"\n                exit 1\n            else\n                # Didn't get it still - bail\n                print_error\n            fi\n        fi\n        echo \"Located Version:  $vers\"\n    else\n        # Got a version passed\n        echo \"User-Provided Version:  $vers\"\n    fi\n    echo \"Building download url...\"\n    url=\"$(\\\n    curl -L https://www.python.org/downloads/release/python-${vers//./}/ --compressed 2>&1 | \\\n    grep -iE \"python-$vers-macos.*.pkg\\\"\" | \\\n    grep -iE \"a href=\" | \\\n    awk -F'\"' '{ print $2 }' | \\\n    head -n 1\\\n    )\"\n    if [ -z \"$url\" ]; then\n        if [ \"$just_installing\" == \"TRUE\" ]; then\n            echo \" - Failed to build download url!\"\n            exit 1\n        else\n            # Couldn't get the URL - bail\n            print_error\n        fi\n    fi\n    echo \" - $url\"\n    echo \"Downloading...\"\n    # Create a temp dir and download to it\n    tempdir=\"$(mktemp -d 2>/dev/null || mktemp -d -t 'tempdir')\"\n    curl \"$url\" -o \"$tempdir/python.pkg\"\n    if [ \"$?\" != \"0\" ]; then\n        echo \" - Failed to download python installer!\"\n        exit $?\n    fi\n    echo\n    echo \"Running python install package...\"\n    echo\n    sudo installer -pkg \"$tempdir/python.pkg\" -target /\n    if [ \"$?\" != \"0\" ]; then\n        echo \" - Failed to install python!\"\n        exit $?\n    fi\n    echo\n    # Now we expand the package and look for a shell update script\n    pkgutil --expand \"$tempdir/python.pkg\" \"$tempdir/python\"\n    if [ -e \"$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall\" ]; then\n        # Run the script\n        echo \"Updating PATH...\"\n        echo\n        \"$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall\"\n        echo\n    fi\n    vers_folder=\"Python $(echo \"$vers\" | cut -d'.' -f1 -f2)\"\n    if [ -f \"/Applications/$vers_folder/Install Certificates.command\" ]; then\n        # Certs script exists - let's execute that to make sure our certificates are updated\n        echo \"Updating Certificates...\"\n        echo\n        \"/Applications/$vers_folder/Install Certificates.command\"\n        echo\n    fi\n    echo \"Cleaning up...\"\n    cleanup\n    if [ \"$just_installing\" == \"TRUE\" ]; then\n        echo\n        echo \"Done.\"\n    else\n        # Now we check for py again\n        downloaded=\"TRUE\"\n        clear\n        main\n    fi\n}\n\ncleanup () {\n    if [ -d \"$tempdir\" ]; then\n        rm -Rf \"$tempdir\"\n    fi\n}\n\nprint_error() {\n    clear\n    cleanup\n    echo \"  ###                      ###\"\n    echo \" #     Python Not Found     #\"\n    echo \"###                      ###\"\n    echo\n    echo \"Python is not installed or not found in your PATH var.\"\n    echo\n    if [ \"$kernel\" == \"Darwin\" ]; then\n        echo \"Please go to https://www.python.org/downloads/macos/ to\"\n        echo \"download and install the latest version, then try again.\"\n    else\n        echo \"Please install python through your package manager and\"\n        echo \"try again.\"\n    fi\n    echo\n    exit 1\n}\n\nprint_target_missing() {\n    clear\n    cleanup\n    echo \"  ###                      ###\"\n    echo \" #     Target Not Found     #\"\n    echo \"###                      ###\"\n    echo\n    echo \"Could not locate $target!\"\n    echo\n    exit 1\n}\n\nformat_version () {\n    local vers=\"$1\"\n    echo \"$(echo \"$1\" | awk -F. '{ printf(\"%d%03d%03d%03d\\n\", $1,$2,$3,$4); }')\"\n}\n\nvercomp () {\n    # Modified from: https://apple.stackexchange.com/a/123408/11374\n    local ver1=\"$(format_version \"$1\")\" ver2=\"$(format_version \"$2\")\"\n    if [ $ver1 -gt $ver2 ]; then\n        echo \"1\"\n    elif [ $ver1 -lt $ver2 ]; then\n        echo \"2\"\n    else\n        echo \"0\"\n    fi\n}\n\nget_local_python_version() {\n    # $1 = Python bin name (defaults to python3)\n    # Echoes the path to the highest version of the passed python bin if any\n    local py_name=\"$1\" max_version= python= python_version= python_path=\n    if [ -z \"$py_name\" ]; then\n        py_name=\"python3\"\n    fi\n    py_list=\"$(which -a \"$py_name\" 2>/dev/null)\"\n    # Walk that newline separated list\n    while read python; do\n        if [ -z \"$python\" ]; then\n            # Got a blank line - skip\n            continue\n        fi\n        if [ \"$check_py3_stub\" == \"1\" ] && [ \"$python\" == \"/usr/bin/python3\" ]; then\n            # See if we have a valid developer path\n            xcode-select -p > /dev/null 2>&1\n            if [ \"$?\" != \"0\" ]; then\n                # /usr/bin/python3 path - but no valid developer dir\n                continue\n            fi\n        fi\n        python_version=\"$(get_python_version $python)\"\n        if [ -z \"$python_version\" ]; then\n            # Didn't find a py version - skip\n            continue\n        fi\n        # Got the py version - compare to our max\n        if [ -z \"$max_version\" ] || [ \"$(vercomp \"$python_version\" \"$max_version\")\" == \"1\" ]; then\n            # Max not set, or less than the current - update it\n            max_version=\"$python_version\"\n            python_path=\"$python\"\n        fi\n    done <<< \"$py_list\"\n    echo \"$python_path\"\n}\n\nget_python_version() {\n    local py_path=\"$1\" py_version=\n    # Get the python version by piping stderr into stdout (for py2), then grepping the output for\n    # the word \"python\", getting the second element, and grepping for an alphanumeric version number\n    py_version=\"$($py_path -V 2>&1 | grep -i python | cut -d' ' -f2 | grep -E \"[A-Za-z\\d\\.]+\")\"\n    if [ ! -z \"$py_version\" ]; then\n        echo \"$py_version\"\n    fi\n}\n\nprompt_and_download() {\n    if [ \"$downloaded\" != \"FALSE\" ] || [ \"$kernel\" != \"Darwin\" ]; then\n        # We already tried to download, or we're not on macOS - just bail\n        print_error\n    fi\n    clear\n    echo \"  ###                      ###\"\n    echo \" #     Python Not Found     #\"\n    echo \"###                      ###\"\n    echo\n    target_py=\"Python 3\"\n    printed_py=\"Python 2 or 3\"\n    if [ \"$use_py3\" == \"FORCE\" ]; then\n        printed_py=\"Python 3\"\n    elif [ \"$use_py3\" == \"FALSE\" ]; then\n        target_py=\"Python 2\"\n        printed_py=\"Python 2\"\n    fi\n    echo \"Could not locate $printed_py!\"\n    echo\n    echo \"This script requires $printed_py to run.\"\n    echo\n    while true; do\n        read -p \"Would you like to install the latest $target_py now? (y/n):  \" yn\n        case $yn in\n            [Yy]* ) download_py;break;;\n            [Nn]* ) print_error;;\n        esac\n    done\n}\n\nmain() {\n    local python= version=\n    # Verify our target exists\n    if [ ! -f \"$dir/$target\" ]; then\n        # Doesn't exist\n        print_target_missing\n    fi\n    if [ -z \"$use_py3\" ]; then\n        use_py3=\"TRUE\"\n    fi\n    if [ \"$use_py3\" != \"FALSE\" ]; then\n        # Check for py3 first\n        python=\"$(get_local_python_version python3)\"\n    fi\n    if [ \"$use_py3\" != \"FORCE\" ] && [ -z \"$python\" ]; then\n        # We aren't using py3 explicitly, and we don't already have a path\n        python=\"$(get_local_python_version python2)\"\n        if [ -z \"$python\" ]; then\n            # Try just looking for \"python\"\n            python=\"$(get_local_python_version python)\"\n        fi\n    fi\n    if [ -z \"$python\" ]; then\n        # Didn't ever find it - prompt\n        prompt_and_download\n        return 1\n    fi\n    # Found it - start our script and pass all args\n    \"$python\" \"$dir/$target\" \"${args[@]}\"\n}\n\n# Keep track of whether or not we're on macOS to determine if\n# we can download and install python for the user as needed.\nkernel=\"$(uname -s)\"\n# Check to see if we need to force based on\n# macOS version. 10.15 has a dummy python3 version\n# that can trip up some py3 detection in other scripts.\n# set_use_py3_if \"3\" \"10.15\" \"FORCE\"\ndownloaded=\"FALSE\"\n# Check for the aforementioned /usr/bin/python3 stub if\n# our OS version is 10.15 or greater.\ncheck_py3_stub=\"$(compare_to_version \"3\" \"10.15\")\"\ntrap cleanup EXIT\nif [ \"$1\" == \"--install-python\" ] && [ \"$kernel\" == \"Darwin\" ]; then\n    just_installing=\"TRUE\"\n    download_py \"$2\"\nelse\n    main\nfi\n"
  },
  {
    "path": "gibMacOS.py",
    "content": "#!/usr/bin/env python3\nfrom Scripts import downloader,utils,run,plist\nimport os, shutil, time, sys, argparse, re, json, subprocess\n\nclass ProgramError(Exception):\n    def __init__(self, message, title = \"Error\"):\n        super(Exception, self).__init__(message)\n        self.title = title\n\n\nclass gibMacOS:\n    def __init__(self, interactive = True, download_dir = None):\n        self.interactive = interactive\n        self.download_dir = download_dir\n        self.d = downloader.Downloader()\n        self.u = utils.Utils(\"gibMacOS\", interactive=interactive)\n        self.r = run.Run()\n        self.min_w = 80\n        self.min_h = 24\n        if os.name == \"nt\":\n            self.min_w = 120\n            self.min_h = 30\n        self.resize()\n\n        self.catalog_suffix = {\n            \"public\" : \"beta\",\n            \"publicrelease\" : \"\",\n            \"customer\" : \"customerseed\",\n            \"developer\" : \"seed\"\n        }\n        \n        # Load settings.json if it exists in the Scripts folder\n        self.settings_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),\"Scripts\",\"settings.json\")\n        self.settings = {}\n        if os.path.exists(self.settings_path):\n            try: self.settings = json.load(open(self.settings_path))\n            except: pass\n\n        # Load prod_cache.json if it exists in the Scripts folder\n        self.prod_cache_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),\"Scripts\",\"prod_cache.plist\")\n        self.prod_cache = {}\n        if os.path.exists(self.prod_cache_path):\n            try:\n                with open(self.prod_cache_path,\"rb\") as f:\n                    self.prod_cache = plist.load(f)\n                assert isinstance(self.prod_cache,dict)\n            except:\n                self.prod_cache = {}\n        \n        # If > 16, assume X-5, else 10.X\n        # e.g. 17 = Monterey, 18 = Ventura, 19 = Sonoma, 20 = Sequoia\n        self.current_macos = self.settings.get(\"current_macos\",20)\n        self.min_macos = 5\n        self.print_urls = self.settings.get(\"print_urls\",False)\n        self.print_json = False\n        self.hide_pid = self.settings.get(\"hide_pid\",False)\n        self.mac_os_names_url = {\n            \"8\" : \"mountainlion\",\n            \"7\" : \"lion\",\n            \"6\" : \"snowleopard\",\n            \"5\" : \"leopard\"\n        }\n        self.version_names = {\n            \"tiger\" : \"10.4\",\n            \"leopard\" : \"10.5\",\n            \"snow leopard\" : \"10.6\",\n            \"lion\" : \"10.7\",\n            \"mountain lion\" : \"10.8\",\n            \"mavericks\" : \"10.9\",\n            \"yosemite\" : \"10.10\",\n            \"el capitan\" : \"10.11\",\n            \"sierra\" : \"10.12\",\n            \"high sierra\" : \"10.13\",\n            \"mojave\" : \"10.14\",\n            \"catalina\" : \"10.15\",\n            \"big sur\" : \"11\",\n            \"monterey\" : \"12\",\n            \"ventura\" : \"13\",\n            \"sonoma\" : \"14\",\n            \"sequoia\" : \"15\",\n            \"tahoe\" : \"26\"\n        }\n        self.current_catalog = self.settings.get(\"current_catalog\",\"publicrelease\")\n        self.catalog_data    = None\n        self.scripts = \"Scripts\"\n        self.local_catalog = os.path.join(os.path.dirname(os.path.realpath(__file__)),self.scripts,\"sucatalog.plist\")\n        self.caffeinate_downloads = self.settings.get(\"caffeinate_downloads\",True)\n        self.caffeinate_process = None\n        self.save_local = False\n        self.force_local = False\n        self.find_recovery = self.settings.get(\"find_recovery\",False)\n        self.recovery_suffixes = (\n            \"RecoveryHDUpdate.pkg\",\n            \"RecoveryHDMetaDmg.pkg\"\n        )\n        self.settings_to_save = (\n            \"current_macos\",\n            \"current_catalog\",\n            \"print_urls\",\n            \"find_recovery\",\n            \"hide_pid\",\n            \"caffeinate_downloads\"\n        )\n        self.mac_prods = []\n\n    def resize(self, width=0, height=0):\n        if not self.interactive:\n            return\n        width = width if width > self.min_w else self.min_w\n        height = height if height > self.min_h else self.min_h\n        self.u.resize(width, height)\n\n    def save_settings(self):\n        # Ensure we're using the latest values\n        for setting in self.settings_to_save:\n            self.settings[setting] = getattr(self,setting,None)\n        try:\n            json.dump(self.settings,open(self.settings_path,\"w\"),indent=2)\n        except Exception as e:\n            raise ProgramError(\n                    \"Failed to save settings to:\\n\\n{}\\n\\nWith error:\\n\\n - {}\\n\".format(self.settings_path,repr(e)),\n                    title=\"Error Saving Settings\")\n\n    def save_prod_cache(self):\n        try:\n            with open(self.prod_cache_path,\"wb\") as f:\n                plist.dump(self.prod_cache,f)\n        except Exception as e:\n            raise ProgramError(\n                    \"Failed to save product cache to:\\n\\n{}\\n\\nWith error:\\n\\n - {}\\n\".format(self.prod_cache_path,repr(e)),\n                    title=\"Error Saving Product Cache\")\n\n    def set_prods(self):\n        self.resize()\n        if not self.get_catalog_data(self.save_local):\n            message = \"The currently selected catalog ({}) was not reachable\\n\".format(self.current_catalog)\n            if self.save_local:\n                message += \"and I was unable to locate a valid catalog file at:\\n - {}\\n\".format(\n                    self.local_catalog\n                )\n            message += \"Please ensure you have a working internet connection.\"\n            if self.interactive:\n                print(message)\n                self.u.grab(\"\\nPress [enter] to continue...\")\n                return\n            raise ProgramError(message, title=\"Catalog Data Error\")\n        self.u.head(\"Parsing Data\")\n        self.u.info(\"Scanning products after catalog download...\\n\")\n        self.mac_prods = self.get_dict_for_prods(self.get_installers())\n\n    def set_catalog(self, catalog):\n        self.current_catalog = catalog.lower() if catalog.lower() in self.catalog_suffix else \"publicrelease\"\n\n    def num_to_macos(self,macos_num,for_url=True):\n        if for_url: # Resolve 8-5 to their names and show Big Sur as 10.16\n            return self.mac_os_names_url.get(str(macos_num),\"10.{}\".format(macos_num)) if macos_num <= 16 else str(macos_num-5)\n        # Return 10.xx for anything Catalina and lower, otherwise 11+\n        return \"10.{}\".format(macos_num) if macos_num <= 15 else str(macos_num-5)\n\n    def macos_to_num(self,macos):\n        try:\n            macos_parts = [int(x) for x in macos.split(\".\")][:2 if macos.startswith(\"10.\") else 1]\n            if macos_parts[0] == 11: macos_parts = [10,16] # Big sur\n        except:\n            return None\n        if len(macos_parts) > 1: return macos_parts[1]\n        return 5+macos_parts[0]\n\n    def get_macos_versions(self,minos=None,maxos=None,catalog=\"\"):\n        if minos is None: minos = self.min_macos\n        if maxos is None: maxos = self.current_macos\n        if minos > maxos: minos,maxos = maxos,minos # Ensure min is less than or equal\n        os_versions = [self.num_to_macos(x,for_url=True) for x in range(minos,min(maxos+1,21))] # until sequoia\n        if maxos > 30: # since tahoe\n            os_versions.extend([self.num_to_macos(x,for_url=True) for x in range(31,maxos+1)])\n        if catalog:\n            # We have a custom catalog - prepend the first entry + catalog to the list\n            custom_cat_entry = os_versions[-1]+catalog\n            os_versions.append(custom_cat_entry)\n        return os_versions\n\n    def build_url(self, **kwargs):\n        catalog = kwargs.get(\"catalog\", self.current_catalog).lower()\n        catalog = catalog if catalog.lower() in self.catalog_suffix else \"publicrelease\"\n        version = int(kwargs.get(\"version\", self.current_macos))\n        return \"https://swscan.apple.com/content/catalogs/others/index-{}.merged-1.sucatalog\".format(\n            \"-\".join(reversed(self.get_macos_versions(self.min_macos,version,catalog=self.catalog_suffix.get(catalog,\"\"))))\n        )\n\n    def get_catalog_data(self, local = False):\n        # Gets the data based on our current_catalog\n        url = self.build_url(catalog=self.current_catalog, version=self.current_macos)\n        self.u.head(\"Downloading Catalog\")\n        if local:\n            self.u.info(\"Checking for:\\n - {}\".format(\n                self.local_catalog\n            ))\n            if os.path.exists(self.local_catalog):\n                self.u.info(\" - Found - loading...\")\n                try:\n                    with open(self.local_catalog, \"rb\") as f:\n                        self.catalog_data = plist.load(f)\n                        assert isinstance(self.catalog_data,dict)\n                    return True\n                except Exception as e:\n                    self.u.info(\" - Error loading: {}\".format(e))\n                    self.u.info(\" - Downloading instead...\\n\")\n            else:\n                self.u.info(\" - Not found - downloading instead...\\n\")\n        self.u.info(\"Currently downloading {} catalog from:\\n\\n{}\\n\".format(self.current_catalog, url))\n        try:\n            b = self.d.get_bytes(url, self.interactive)\n            self.u.info(\"\")\n            self.catalog_data = plist.loads(b)\n        except:\n            self.u.info(\"Error downloading!\")\n            return False\n        try:\n            # Assume it's valid data - dump it to a local file\n            if local or self.force_local:\n                self.u.info(\" - Saving to:\\n - {}\".format(\n                    self.local_catalog\n                ))\n                with open(self.local_catalog, \"wb\") as f:\n                    plist.dump(self.catalog_data, f)\n        except Exception as e:\n            self.u.info(\" - Error saving: {}\".format(e))\n            return False\n        return True\n\n    def get_installers(self, plist_dict = None):\n        if not plist_dict:\n            plist_dict = self.catalog_data\n        if not plist_dict:\n            return []\n        mac_prods = []\n        for p in plist_dict.get(\"Products\", {}):\n            if not self.find_recovery:\n                val = plist_dict.get(\"Products\",{}).get(p,{}).get(\"ExtendedMetaInfo\",{}).get(\"InstallAssistantPackageIdentifiers\",{})\n                if val.get(\"OSInstall\",{}) == \"com.apple.mpkg.OSInstall\" or val.get(\"SharedSupport\",\"\").startswith(\"com.apple.pkg.InstallAssistant\"):\n                    mac_prods.append(p)\n            else:\n                # Find out if we have any of the recovery_suffixes\n                if any(x for x in plist_dict.get(\"Products\",{}).get(p,{}).get(\"Packages\",[]) if x[\"URL\"].endswith(self.recovery_suffixes)):\n                    mac_prods.append(p)\n        return mac_prods\n\n    def get_build_version(self, dist_dict):\n        build = version = name = \"Unknown\"\n        try:\n            dist_url = dist_dict.get(\"English\",dist_dict.get(\"en\",\"\"))\n            dist_file = self.d.get_string(dist_url,False)\n            assert isinstance(dist_file,str)\n        except:\n            dist_file = \"\"\n        build_search = \"macOSProductBuildVersion\" if \"macOSProductBuildVersion\" in dist_file else \"BUILD\"\n        vers_search  = \"macOSProductVersion\" if \"macOSProductVersion\" in dist_file else \"VERSION\"\n        try:\n            build = dist_file.split(\"<key>{}</key>\".format(build_search))[1].split(\"<string>\")[1].split(\"</string>\")[0]\n        except:\n            pass\n        try:\n            version = dist_file.split(\"<key>{}</key>\".format(vers_search))[1].split(\"<string>\")[1].split(\"</string>\")[0]\n        except:\n            pass\n        try:\n            name = re.search(r\"<title>(.+?)</title>\",dist_file).group(1)\n        except:\n            pass\n        try:\n            # XXX: This is parsing a JavaScript array from the script part of the dist file.\n            device_ids = re.search(r\"var supportedDeviceIDs\\s*=\\s*\\[([^]]+)\\];\", dist_file)[1]\n            device_ids = list(set(i.lower() for i in re.findall(r\"'([^',]+)'\", device_ids)))\n        except:\n            device_ids = []\n        return (build,version,name,device_ids)\n\n    def get_dict_for_prods(self, prods, plist_dict = None):\n        plist_dict = plist_dict or self.catalog_data or {}\n        prod_list = []\n        # Keys required to consider a cached element valid\n        prod_keys = (\n            \"build\",\n            \"date\",\n            \"description\",\n            \"device_ids\",\n            \"installer\",\n            \"product\",\n            \"time\",\n            \"title\",\n            \"version\",\n        )\n\n        def get_packages_and_size(plist_dict,prod,recovery):\n            # Iterate the available packages and save their urls and sizes\n            packages = []\n            size = -1\n            if recovery:\n                # Only get the recovery packages\n                packages = [x for x in plist_dict.get(\"Products\",{}).get(prod,{}).get(\"Packages\",[]) if x[\"URL\"].endswith(self.recovery_suffixes)]\n            else:\n                # Add them all!\n                packages = plist_dict.get(\"Products\",{}).get(prod,{}).get(\"Packages\",[])\n            # Get size\n            size = self.d.get_size(sum([i[\"Size\"] for i in packages]))\n            return (packages,size)\n\n        def print_prod(prod,prod_list):\n            self.u.info(\" -->{}. {} ({}){}\".format(\n                str(len(prod_list)+1).rjust(3),\n                prod[\"title\"],\n                prod[\"build\"],\n                \" - FULL Install\" if self.find_recovery and prod[\"installer\"] else \"\"\n            ))\n\n        def prod_valid(prod,prod_list,prod_keys):\n            # Check if the prod has all prod keys, and\n            # none are \"Unknown\"\n            if not isinstance(prod_list,dict) or not prod in prod_list or \\\n            not all(x in prod_list[prod] for x in prod_keys):\n                # Wrong type, missing the prod, or prod_list keys\n                return False\n            # Let's make sure none of the keys return Unknown\n            if any(prod_list[prod].get(x,\"Unknown\")==\"Unknown\" for x in prod_keys):\n                return False\n            return True\n\n        # Boolean to keep track of cache updates\n        prod_changed = False\n        for prod in prods:\n            if prod_valid(prod,self.prod_cache,prod_keys):\n                # Already have it - and it's valid.\n                # Create a shallow copy\n                prodd = {}\n                for key in self.prod_cache[prod]:\n                    prodd[key] = self.prod_cache[prod][key]\n                # Update the packages and size lists\n                prodd[\"packages\"],prodd[\"size\"] = get_packages_and_size(plist_dict,prod,self.find_recovery)\n                # Add to our list and continue on\n                prod_list.append(prodd)\n                # Log the product\n                print_prod(prodd,prod_list)\n                continue\n            # Grab the ServerMetadataURL for the passed product key if it exists\n            prodd = {\"product\":prod}\n            try:\n                url = plist_dict.get(\"Products\",{}).get(prod,{}).get(\"ServerMetadataURL\",\"\")\n                assert url\n                b = self.d.get_bytes(url,False)\n                smd = plist.loads(b)\n            except:\n                smd = {}\n            # Populate some info!\n            prodd[\"date\"] = plist_dict.get(\"Products\",{}).get(prod,{}).get(\"PostDate\",\"\")\n            prodd[\"installer\"] = plist_dict.get(\"Products\",{}).get(prod,{}).get(\"ExtendedMetaInfo\",{}).get(\"InstallAssistantPackageIdentifiers\",{}).get(\"OSInstall\",{}) == \"com.apple.mpkg.OSInstall\"\n            prodd[\"time\"] = time.mktime(prodd[\"date\"].timetuple()) + prodd[\"date\"].microsecond / 1E6\n            prodd[\"version\"] = smd.get(\"CFBundleShortVersionString\",\"Unknown\").strip()\n            # Try to get the description too\n            try:\n                desc = smd.get(\"localization\",{}).get(\"English\",{}).get(\"description\",\"\").decode(\"utf-8\")\n                desctext = desc.split('\"p1\">')[1].split(\"</a>\")[0]\n            except:\n                desctext = \"\"\n            prodd[\"description\"] = desctext\n            prodd[\"packages\"],prodd[\"size\"] = get_packages_and_size(plist_dict,prod,self.find_recovery)\n            # Get size\n            prodd[\"size\"] = self.d.get_size(sum([i[\"Size\"] for i in prodd[\"packages\"]]))\n            # Attempt to get the build/version/name/device-ids info from the dist\n            prodd[\"build\"],v,n,prodd[\"device_ids\"] = self.get_build_version(plist_dict.get(\"Products\",{}).get(prod,{}).get(\"Distributions\",{}))\n            prodd[\"title\"] = smd.get(\"localization\",{}).get(\"English\",{}).get(\"title\",n)\n            if v.lower() != \"unknown\":\n                prodd[\"version\"] = v\n            prod_list.append(prodd)\n            # If we were able to resolve the SMD URL - or it didn't exist, save it to the cache\n            if smd or not plist_dict.get(\"Products\",{}).get(prod,{}).get(\"ServerMetadataURL\",\"\"):\n                prod_changed = True\n                # Create a temp prod dict so we can save all but the packages and\n                # size keys - as those are determined based on self.find_recovery\n                temp_prod = {}\n                for key in prodd:\n                    if key in (\"packages\",\"size\"): continue\n                    if prodd[key] == \"Unknown\":\n                        # Don't cache Unknown values\n                        temp_prod = None\n                        break\n                    temp_prod[key] = prodd[key]\n                if temp_prod:\n                    # Only update the cache if it changed\n                    self.prod_cache[prod] = temp_prod\n            # Log the product\n            print_prod(prodd,prod_list)\n        # Try saving the cache for later\n        if prod_changed and self.prod_cache:\n            try: self.save_prod_cache()\n            except: pass\n        # Sort by newest\n        prod_list = sorted(prod_list, key=lambda x:x[\"time\"], reverse=True)\n        return prod_list\n\n    def start_caffeinate(self):\n        # Check if we need to caffeinate\n        if sys.platform.lower() == \"darwin\" \\\n        and self.caffeinate_downloads \\\n        and os.path.isfile(\"/usr/bin/caffeinate\"):\n            # Terminate any existing caffeinate process\n            self.term_caffeinate_proc()\n            # Create a new caffeinate process\n            self.caffeinate_process = subprocess.Popen(\n                [\"/usr/bin/caffeinate\"],\n                stderr=getattr(subprocess,\"DEVNULL\",open(os.devnull,\"w\")),\n                stdout=getattr(subprocess,\"DEVNULL\",open(os.devnull,\"w\")),\n                stdin=getattr(subprocess,\"DEVNULL\",open(os.devnull,\"w\"))\n            )\n        return self.caffeinate_process\n\n    def term_caffeinate_proc(self):\n        if self.caffeinate_process is None:\n            return True\n        try:\n            if self.caffeinate_process.poll() is None:\n                # Save the time we started waiting\n                start = time.time()\n                while self.caffeinate_process.poll() is None:\n                    # Make sure we haven't waited too long\n                    if time.time() - start > 10:\n                        print(\" - Timed out trying to terminate caffeinate process with PID {}!\".format(\n                            self.caffeinate_process.pid\n                        ))\n                        return False\n                    # It's alive - terminate it\n                    self.caffeinate_process.terminate()\n                    # Sleep to let things settle\n                    time.sleep(0.02)\n        except:\n            pass\n        return True # Couldn't poll - or we termed it\n\n    def download_prod(self, prod, dmg = False):\n        # Takes a dictonary of details and downloads it\n        self.resize()\n        name = \"{} - {} {} ({})\".format(prod[\"product\"], prod[\"version\"], prod[\"title\"], prod[\"build\"]).replace(\":\",\"\").strip()\n        download_dir = self.download_dir or os.path.join(os.path.dirname(os.path.realpath(__file__)), \"macOS Downloads\", self.current_catalog, name)\n        dl_list = []\n        for x in prod[\"packages\"]:\n            if not x.get(\"URL\",None):\n                continue\n            if dmg and not x.get(\"URL\",\"\").lower().endswith(\".dmg\"):\n                continue\n            # add it to the list\n            dl_list.append(x)\n        if not len(dl_list):\n            raise ProgramError(\"There were no files to download\")\n        done = []\n        if self.print_json:\n            print(self.product_to_json(prod))\n            if self.interactive:\n                print(\"\")\n                self.u.grab(\"Press [enter] to return...\")\n            return\n        elif self.print_urls:\n            self.u.head(\"Download Links\")\n            print(\"{}:\\n\".format(name))\n            print(\"\\n\".join([\" - {} ({}) \\n   --> {}\".format(\n                os.path.basename(x[\"URL\"]),\n                self.d.get_size(x[\"Size\"],strip_zeroes=True) if x.get(\"Size\") is not None else \"?? MB\",\n                x[\"URL\"]\n            ) for x in dl_list]))\n            if self.interactive:\n                print(\"\")\n                self.u.grab(\"Press [enter] to return...\")\n            return\n        # Only check the dirs if we need to\n        if self.download_dir is None and os.path.exists(download_dir):\n            while True:\n                self.u.head(\"Already Exists\")\n                self.u.info(\"It looks like you've already downloaded the following package:\\n{}\\n\".format(name))\n                if not self.interactive:\n                    menu = \"r\"\n                else:\n                    print(\"R. Resume Incomplete Files\")\n                    print(\"D. Redownload All Files\")\n                    print(\"\")\n                    print(\"M. Return\")\n                    print(\"Q. Quit\")\n                    print(\"\")\n                    menu = self.u.grab(\"Please select an option:  \")\n                if not len(menu):\n                    continue\n                elif menu.lower() == \"q\":\n                    self.u.custom_quit()\n                elif menu.lower() == \"m\":\n                    return\n                elif menu.lower() == \"r\":\n                    break\n                elif menu.lower() == \"d\":\n                    # Remove the old copy, then re-download\n                    shutil.rmtree(download_dir)\n                    break\n        # Make it anew as needed\n        if not os.path.isdir(download_dir):\n            os.makedirs(download_dir)\n        # Clean up any leftover or missed caffeinate\n        # procs\n        self.term_caffeinate_proc()\n        for c,x in enumerate(dl_list,start=1):\n            url = x[\"URL\"]\n            self.u.head(\"Downloading File {} of {}\".format(c, len(dl_list)))\n            self.u.info(\"- {} -\\n\".format(name))\n            if len(done):\n                self.u.info(\"\\n\".join([\"{} --> {}\".format(y[\"name\"], \"Succeeded\" if y[\"status\"] else \"Failed\") for y in done]))\n                self.u.info(\"\")\n            if dmg:\n                self.u.info(\"NOTE: Only Downloading DMG Files\\n\")\n            self.u.info(\"Downloading {}...\\n\".format(os.path.basename(url)))\n            try:\n                # Caffeinate as needed\n                self.start_caffeinate()\n                result = self.d.stream_to_file(url, os.path.join(download_dir, os.path.basename(url)), allow_resume=True)\n                assert result is not None\n                done.append({\"name\":os.path.basename(url), \"status\":True})\n            except:\n                done.append({\"name\":os.path.basename(url), \"status\":False})\n            # Kill caffeinate if we need to\n            self.term_caffeinate_proc()\n        succeeded = [x for x in done if x[\"status\"]]\n        failed    = [x for x in done if not x[\"status\"]]\n        self.u.head(\"Downloaded {} of {}\".format(len(succeeded), len(dl_list)))\n        self.u.info(\"- {} -\\n\".format(name))\n        self.u.info(\"Succeeded:\")\n        if len(succeeded):\n            for x in succeeded:\n                self.u.info(\"  {}\".format(x[\"name\"]))\n        else:\n            self.u.info(\"  None\")\n        self.u.info(\"\\nFailed:\")\n        if len(failed):\n            for x in failed:\n                self.u.info(\"  {}\".format(x[\"name\"]))\n        else:\n            self.u.info(\"  None\")\n        self.u.info(\"\\nFiles saved to:\\n  {}\\n\".format(download_dir))\n        if self.interactive:\n            self.u.grab(\"Press [enter] to return...\")\n        elif len(failed):\n            raise ProgramError(\"{} files failed to download\".format(len(failed)))\n\n    def product_to_json(self, prod):\n        prod_dict = {}\n        for key in [\"product\", \"version\", \"build\", \"title\", \"size\", \"packages\"]:\n            if key in prod:\n                prod_dict[key] = prod[key]\n        prod_dict[\"date\"] = prod[\"date\"].isoformat()\n        prod_dict[\"deviceIds\"] = list(prod[\"device_ids\"])\n        return json.dumps(prod_dict,indent=2)\n\n    def show_catalog_url(self):\n        self.resize()\n        self.u.head()\n        print(\"Current Catalog:   {}\".format(self.current_catalog))\n        print(\"Max macOS Version: {}\".format(self.num_to_macos(self.current_macos,for_url=False)))\n        print(\"\")\n        print(\"{}\".format(self.build_url()))\n        if self.interactive:\n            print(\"\")\n            self.u.grab(\"Press [enter] to return...\")\n\n    def pick_catalog(self):\n        self.resize()\n        self.u.head(\"Select SU Catalog\")\n        count = 0\n        for x in self.catalog_suffix:\n            count += 1\n            print(\"{}. {}\".format(count, x))\n        print(\"\")\n        print(\"M. Main Menu\")\n        print(\"Q. Quit\")\n        print(\"\")\n        menu = self.u.grab(\"Please select an option:  \")\n        if not len(menu):\n            self.pick_catalog()\n            return\n        if menu[0].lower() == \"m\":\n            return\n        elif menu[0].lower() == \"q\":\n            self.u.custom_quit()\n        # Should have something to test here\n        try:\n            i = int(menu)\n            self.current_catalog = list(self.catalog_suffix)[i-1]\n            self.save_settings()\n        except:\n            # Incorrect - try again\n            self.pick_catalog()\n            return\n        # If we made it here - then we got something\n        # Reload with the proper catalog\n        self.get_catalog_data()\n\n    def pick_macos(self):\n        self.resize()\n        self.u.head(\"Select Max macOS Version\")\n        print(\"Currently set to {}\".format(self.num_to_macos(self.current_macos,for_url=False)))\n        print(\"\")\n        print(\"M. Main Menu\")\n        print(\"Q. Quit\")\n        print(\"\")\n        print(\"Please type the max macOS version for the catalog url\")\n        menu = self.u.grab(\"eg. 10.15 for Catalina, 11 for Big Sur, 12 for Monterey:  \")\n        if not len(menu):\n            self.pick_macos()\n            return\n        if menu[0].lower() == \"m\":\n            return\n        elif menu[0].lower() == \"q\":\n            self.u.custom_quit()\n        # At this point - we should have something in the proper format\n        version = self.macos_to_num(menu)\n        if not version: return\n        self.current_macos = version\n        self.save_settings()\n        # At this point, we should be good - set teh catalog\n        # data - but if it fails, remove the listed prods\n        if not self.get_catalog_data():\n            self.catalog_data = None\n            self.u.grab(\"\\nPress [enter] to return...\")\n            self.pick_macos()\n            return\n\n    def main(self, dmg = False):\n        lines = []\n        lines.append(\"Available Products:\")\n        lines.append(\" \")\n        if not len(self.mac_prods):\n            lines.append(\"No installers in catalog!\")\n            lines.append(\" \")\n        for num,p in enumerate(self.mac_prods,start=1):\n            var1 = \"{}. {} {}\".format(str(num).rjust(2), p[\"title\"], p[\"version\"])\n            var2 = \"\"\n            if p[\"build\"].lower() != \"unknown\":\n                var1 += \" ({})\".format(p[\"build\"])\n            if not self.hide_pid:\n                var2 = \"   - {} - Added {} - {}\".format(p[\"product\"], p[\"date\"], p[\"size\"])\n            if self.find_recovery and p[\"installer\"]:\n                # Show that it's a full installer\n                if self.hide_pid:\n                    var1 += \" - FULL Install\"\n                else:\n                    var2 += \" - FULL Install\"\n            lines.append(var1)\n            if not self.hide_pid:\n                lines.append(var2)\n        lines.append(\" \")\n        lines.append(\"M. Change Max-OS Version (Currently {})\".format(self.num_to_macos(self.current_macos,for_url=False)))\n        lines.append(\"C. Change Catalog (Currently {})\".format(self.current_catalog))\n        lines.append(\"I. Only Print URLs (Currently {})\".format(\"On\" if self.print_urls else \"Off\"))\n        lines.append(\"H. {} Package IDs and Upload Dates\".format(\"Show\" if self.hide_pid else \"Hide\"))\n        if sys.platform.lower() == \"darwin\":\n            lines.append(\"S. Set Current Catalog to SoftwareUpdate Catalog\")\n            lines.append(\"L. Clear SoftwareUpdate Catalog\")\n            lines.append(\"F. Caffeinate Downloads to Prevent Sleep (Currently {})\".format(\"On\" if self.caffeinate_downloads else \"Off\"))\n        lines.append(\"R. Toggle Recovery-Only (Currently {})\".format(\"On\" if self.find_recovery else \"Off\"))\n        lines.append(\"U. Show Catalog URL\")\n        lines.append(\"Q. Quit\")\n        lines.append(\" \")\n        self.resize(len(max(lines)), len(lines)+5)\n        self.u.head()\n        print(\"\\n\".join(lines))\n        menu = self.u.grab(\"Please select an option:  \")\n        if not len(menu):\n            return\n        if menu[0].lower() == \"q\":\n            self.resize()\n            self.u.custom_quit()\n        elif menu[0].lower() == \"u\":\n            self.show_catalog_url()\n        elif menu[0].lower() == \"m\":\n            self.pick_macos()\n        elif menu[0].lower() == \"c\":\n            self.pick_catalog()\n        elif menu[0].lower() == \"i\":\n            self.print_urls ^= True\n            self.save_settings()\n        elif menu[0].lower() == \"h\":\n            self.hide_pid ^= True\n            self.save_settings()\n        elif menu[0].lower() == \"s\" and sys.platform.lower() == \"darwin\":\n            # Set the software update catalog to our current catalog url\n            self.u.head(\"Setting SU CatalogURL\")\n            url = self.build_url(catalog=self.current_catalog, version=self.current_macos)\n            print(\"Setting catalog URL to:\\n{}\".format(url))\n            print(\"\")\n            print(\"sudo softwareupdate --set-catalog {}\".format(url))\n            self.r.run({\"args\":[\"softwareupdate\",\"--set-catalog\",url],\"sudo\":True})\n            print(\"\")\n            self.u.grab(\"Done\",timeout=5)\n        elif menu[0].lower() == \"l\" and sys.platform.lower() == \"darwin\":\n            # Clear the software update catalog\n            self.u.head(\"Clearing SU CatalogURL\")\n            print(\"sudo softwareupdate --clear-catalog\")\n            self.r.run({\"args\":[\"softwareupdate\",\"--clear-catalog\"],\"sudo\":True})\n            print(\"\")\n            self.u.grab(\"Done.\", timeout=5)\n        elif menu[0].lower() == \"f\" and sys.platform.lower() == \"darwin\":\n            # Toggle our caffeinate downloads value and save settings\n            self.caffeinate_downloads ^= True\n            self.save_settings()\n        elif menu[0].lower() == \"r\":\n            self.find_recovery ^= True\n            self.save_settings()\n        if menu[0].lower() in [\"m\",\"c\",\"r\"]:\n            self.resize()\n            self.u.head(\"Parsing Data\")\n            print(\"Re-scanning products after url preference toggled...\\n\")\n            self.mac_prods = self.get_dict_for_prods(self.get_installers())\n        else:\n            # Assume we picked something\n            try:\n                menu = int(menu)\n            except:\n                return\n            if menu < 1 or menu > len(self.mac_prods):\n                return\n            self.download_prod(self.mac_prods[menu-1], dmg)\n\n    def get_latest(self, device_id = None, dmg = False):\n        self.u.head(\"Downloading Latest\")\n        prods = sorted(self.mac_prods, key=lambda x:x['version'], reverse=True)\n        if device_id:\n            prod = next(p for p in prods if device_id.lower() in p[\"device_ids\"])\n            if not prod:\n                raise ProgramError(\"No version found for Device ID '{}'\".format(device_id))\n        else:\n            prod = prods[0]\n        self.download_prod(prod, dmg)\n\n    def get_for_product(self, prod, dmg = False):\n        self.u.head(\"Downloading for {}\".format(prod))\n        for p in self.mac_prods:\n            if p[\"product\"] == prod:\n                self.download_prod(p, dmg)\n                return\n        raise ProgramError(\"{} not found\".format(prod))\n\n    def get_for_version(self, vers, build = None, device_id = None, dmg = False):\n        self.u.head(\"Downloading for {} {}\".format(vers, build or \"\"))\n        # Map the versions to their names\n        v = self.version_names.get(vers.lower(),vers.lower())\n        v_dict = {}\n        for n in self.version_names:\n            v_dict[self.version_names[n]] = n\n        n = v_dict.get(v, v)\n        for p in sorted(self.mac_prods, key=lambda x:x['version'], reverse=True):\n            if build and p[\"build\"] != build:\n                continue\n            if device_id and device_id.lower() not in p[\"device_ids\"]:\n                continue\n            pt = p[\"title\"].lower()\n            pv = p[\"version\"].lower()\n            # Need to compare verisons - n = name, v = version\n            # p[\"version\"] and p[\"title\"] may contain either the version\n            # or name - so check both\n            # We want to make sure, if we match the name to the title, that we only match\n            # once - so Sierra/High Sierra don't cross-match\n            #\n            # First check if p[\"version\"] isn't \" \" or \"1.0\"\n            if not pv in [\" \",\"1.0\"]:\n                # Have a real version - match this first\n                if pv.startswith(v):\n                    self.download_prod(p, dmg)\n                    return\n            # Didn't match the version - or version was bad, let's check\n            # the title\n            # Need to make sure n is in the version name, but not equal to it,\n            # and the version name is in p[\"title\"] to disqualify\n            # i.e. - \"Sierra\" exists in \"High Sierra\", but does not equal \"High Sierra\"\n            # and \"High Sierra\" is in \"macOS High Sierra 10.13.6\" - This would match\n            name_match = [x for x in self.version_names if n in x and x != n and x in pt]\n            if (n in pt) and not len(name_match):\n                self.download_prod(p, dmg)\n                return\n        raise ProgramError(\"'{}' '{}' not found\".format(vers, build or \"\"))\n\nif __name__ == '__main__':\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"-l\", \"--latest\", help=\"downloads the version available in the current catalog (overrides --build, --version and --product)\", action=\"store_true\")\n    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\")\n    parser.add_argument(\"-d\", \"--dmg\", help=\"downloads only the .dmg files\", action=\"store_true\")\n    parser.add_argument(\"-s\", \"--savelocal\", help=\"uses a locally saved sucatalog.plist if exists\", action=\"store_true\")\n    parser.add_argument(\"-g\", \"--local-catalog\", help=\"the path to the sucatalog.plist to use (implies --savelocal)\")\n    parser.add_argument(\"-n\", \"--newlocal\", help=\"downloads and saves locally, overwriting any prior sucatalog.plist (will use the path from --local-catalog if provided)\", action=\"store_true\")\n    parser.add_argument(\"-c\", \"--catalog\", help=\"sets the CATALOG to use - publicrelease, public, customer, developer\")\n    parser.add_argument(\"-p\", \"--product\", help=\"sets the product id to search for (overrides --version)\")\n    parser.add_argument(\"-v\", \"--version\", help=\"sets the version of macOS to target - eg '-v 10.14' or '-v Yosemite'\")\n    parser.add_argument(\"-b\", \"--build\", help=\"sets the build of macOS to target - eg '22G120' (must be used together with --version)\")\n    parser.add_argument(\"-m\", \"--maxos\", help=\"sets the max macOS version to consider when building the url - eg 10.14\")\n    parser.add_argument(\"-D\", \"--device-id\", help=\"use with --version or --latest to search for versions supporting the specified Device ID - eg VMM-x86_64 for any x86_64\")\n    parser.add_argument(\"-i\", \"--print-urls\", help=\"only prints the download URLs, does not actually download them\", action=\"store_true\")\n    parser.add_argument(\"-j\", \"--print-json\", help=\"only prints the product metadata in JSON, does not actually download it\", action=\"store_true\")\n    parser.add_argument(\"--no-interactive\", help=\"run in non-interactive mode (auto-enabled when using --product or --version)\", action=\"store_true\")\n    parser.add_argument(\"-o\", \"--download-dir\", help=\"overrides directory where the downloaded files are saved\")\n    args = parser.parse_args()\n\n    if args.build and not (args.latest or args.product or args.version):\n        print(\"The --build option requires a --version\")\n        exit(1)\n\n    interactive = not any((args.no_interactive,args.product,args.version))\n    g = gibMacOS(interactive=interactive, download_dir=args.download_dir)\n\n    if args.recovery:\n        args.dmg = False\n        g.find_recovery = args.recovery\n\n    if args.savelocal:\n        g.save_local = True\n\n    if args.local_catalog:\n        g.save_local = True\n        g.local_catalog = args.local_catalog\n\n    if args.newlocal:\n        g.force_local = True\n\n    if args.print_urls:\n        g.print_urls = True\n\n    if args.print_json:\n        g.print_json = True\n\n    if args.maxos:\n        try:\n            version = g.macos_to_num(args.maxos)\n            if version: g.current_macos = version\n        except:\n            pass\n    if args.catalog:\n        # Set the catalog\n        g.set_catalog(args.catalog)\n\n    try:\n        # Done setting up pre-requisites\n        g.set_prods()\n\n        if args.latest:\n            g.get_latest(device_id=args.device_id, dmg=args.dmg)\n        elif args.product != None:\n            g.get_for_product(args.product, args.dmg)\n        elif args.version != None:\n            g.get_for_version(args.version, args.build, device_id=args.device_id, dmg=args.dmg)\n        elif g.interactive:\n            while True:\n                try:\n                    g.main(args.dmg)\n                except ProgramError as e:\n                    g.u.head(e.title)\n                    print(str(e))\n                    print(\"\")\n                    g.u.grab(\"Press [enter] to return...\")\n        else:\n            raise ProgramError(\"No command specified\")\n    except ProgramError as e:\n        print(str(e))\n        if g.interactive:\n            print(\"\")\n            g.u.grab(\"Press [enter] to exit...\")\n        else:\n            exit(1)\n    exit(0)\n"
  }
]