diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e1c78ce --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Ensure all .bat scripts use CRLF line endings +# This can prevent a number of odd batch issues +*.bat text eol=crlf diff --git a/README.md b/README.md index 0e89048..5edfa33 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A Windows- and OS X-compatible Python script that fetches, from Apple's or your software update server, the Boot Camp ESD ("Electronic Software Distribution") for a specific model of Mac. It unpacks the multiple layers of archives within the flat package and if the script is run on Windows with the `--install` option, it also runs the 64-bit MSI installer. -On Windows, the archives are unpacked using [7-Zip](http://www.7-zip.org), and the 7-Zip MSI is downloaded and installed, and removed later if Brigadier installed it. This tool used to use [dmg2img](http://vu1tur.eu.org/tools/) to perform the extraction of files from Apple's `WindowsSupport.dmg` file, but more recent versions of 7-Zip have included more completely support for DMGs, so dmg2img seems to be no longer needed. +On Windows, the archives are unpacked using [7-Zip](http://www.7-zip.org), and the 7-Zip MSI is downloaded and installed, and removed later if Brigadier installed it. This tool used to use [dmg2img](http://vu1tur.eu.org/tools/) to perform the extraction of files from Apple's `WindowsSupport.dmg` file, but more recent versions of 7-Zip have included more complete support for DMGs, so dmg2img seems to be no longer needed. This was written for two reasons: diff --git a/brigadier.bat b/brigadier.bat new file mode 100644 index 0000000..691aa9f --- /dev/null +++ b/brigadier.bat @@ -0,0 +1,397 @@ +@echo off +REM Get our local path before delayed expansion - allows ! in path +set "thisDir=%~dp0" + +setlocal enableDelayedExpansion +REM Setup initial vars +set "script_name=" +set /a tried=0 +set "toask=yes" +set "pause_on_error=yes" +set "py2v=" +set "py2path=" +set "py3v=" +set "py3path=" +set "pypath=" +set "targetpy=3" + +REM use_py3: +REM TRUE = Use if found, use py2 otherwise +REM FALSE = Use py2 +REM FORCE = Use py3 +set "use_py3=TRUE" + +REM We'll parse if the first argument passed is +REM --install-python and if so, we'll just install +set "just_installing=FALSE" + +REM Get the system32 (or equivalent) path +call :getsyspath "syspath" + +REM Make sure the syspath exists +if "!syspath!" == "" ( + if exist "%SYSTEMROOT%\system32\cmd.exe" ( + if exist "%SYSTEMROOT%\system32\reg.exe" ( + if exist "%SYSTEMROOT%\system32\where.exe" ( + REM Fall back on the default path if it exists + set "ComSpec=%SYSTEMROOT%\system32\cmd.exe" + set "syspath=%SYSTEMROOT%\system32\" + ) + ) + ) + if "!syspath!" == "" ( + cls + echo ### ### + echo # Warning # + echo ### ### + echo. + echo Could not locate cmd.exe, reg.exe, or where.exe + echo. + echo Please ensure your ComSpec environment variable is properly configured and + echo points directly to cmd.exe, then try again. + echo. + echo Current CompSpec Value: "%ComSpec%" + echo. + echo Press [enter] to quit. + pause > nul + exit /b 1 + ) +) + +if "%~1" == "--install-python" ( + set "just_installing=TRUE" + goto installpy +) + +goto checkscript + +:checkscript +REM Check for our script first +set "looking_for=!script_name!" +if "!script_name!" == "" ( + set "looking_for=%~n0.py or %~n0.command" + set "script_name=%~n0.py" + if not exist "!thisDir!\!script_name!" ( + set "script_name=%~n0.command" + ) +) +if not exist "!thisDir!\!script_name!" ( + echo Could not find !looking_for!. + echo Please make sure to run this script from the same directory + echo as !looking_for!. + echo. + echo Press [enter] to quit. + pause > nul + exit /b 1 +) +goto checkpy + +:checkpy +call :updatepath +for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe python 2^> nul`) do ( call :checkpyversion "%%x" "py2v" "py2path" "py3v" "py3path" ) +for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe python3 2^> nul`) do ( call :checkpyversion "%%x" "py2v" "py2path" "py3v" "py3path" ) +for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe py 2^> nul`) do ( call :checkpylauncher "%%x" "py2v" "py2path" "py3v" "py3path" ) +REM Walk our returns to see if we need to install +if /i "!use_py3!" == "FALSE" ( + set "targetpy=2" + set "pypath=!py2path!" +) else if /i "!use_py3!" == "FORCE" ( + set "pypath=!py3path!" +) else if /i "!use_py3!" == "TRUE" ( + set "pypath=!py3path!" + if "!pypath!" == "" set "pypath=!py2path!" +) +if not "!pypath!" == "" ( + goto runscript +) +if !tried! lss 1 ( + if /i "!toask!"=="yes" ( + REM Better ask permission first + goto askinstall + ) else ( + goto installpy + ) +) else ( + cls + echo ### ### + echo # Warning # + echo ### ### + echo. + REM Couldn't install for whatever reason - give the error message + echo Python is not installed or not found in your PATH var. + echo Please install it from https://www.python.org/downloads/windows/ + echo. + echo Make sure you check the box labeled: + echo. + echo "Add Python X.X to PATH" + echo. + echo Where X.X is the py version you're installing. + echo. + echo Press [enter] to quit. + pause > nul + exit /b 1 +) +goto runscript + +:checkpylauncher +REM Attempt to check the latest python 2 and 3 versions via the py launcher +for /f "USEBACKQ tokens=*" %%x in (`%~1 -2 -c "import sys; print(sys.executable)" 2^> nul`) do ( call :checkpyversion "%%x" "%~2" "%~3" "%~4" "%~5" ) +for /f "USEBACKQ tokens=*" %%x in (`%~1 -3 -c "import sys; print(sys.executable)" 2^> nul`) do ( call :checkpyversion "%%x" "%~2" "%~3" "%~4" "%~5" ) +goto :EOF + +:checkpyversion +set "version="&for /f "tokens=2* USEBACKQ delims= " %%a in (`"%~1" -V 2^>^&1`) do ( + REM Ensure we have a version number + call :isnumber "%%a" + if not "!errorlevel!" == "0" goto :EOF + set "version=%%a" +) +if not defined version goto :EOF +if "!version:~0,1!" == "2" ( + REM Python 2 + call :comparepyversion "!version!" "!%~2!" + if "!errorlevel!" == "1" ( + set "%~2=!version!" + set "%~3=%~1" + ) +) else ( + REM Python 3 + call :comparepyversion "!version!" "!%~4!" + if "!errorlevel!" == "1" ( + set "%~4=!version!" + set "%~5=%~1" + ) +) +goto :EOF + +:isnumber +set "var="&for /f "delims=0123456789." %%i in ("%~1") do set var=%%i +if defined var (exit /b 1) +exit /b 0 + +:comparepyversion +REM Exits with status 0 if equal, 1 if v1 gtr v2, 2 if v1 lss v2 +for /f "tokens=1,2,3 delims=." %%a in ("%~1") do ( + set a1=%%a + set a2=%%b + set a3=%%c +) +for /f "tokens=1,2,3 delims=." %%a in ("%~2") do ( + set b1=%%a + set b2=%%b + set b3=%%c +) +if not defined a1 set a1=0 +if not defined a2 set a2=0 +if not defined a3 set a3=0 +if not defined b1 set b1=0 +if not defined b2 set b2=0 +if not defined b3 set b3=0 +if %a1% gtr %b1% exit /b 1 +if %a1% lss %b1% exit /b 2 +if %a2% gtr %b2% exit /b 1 +if %a2% lss %b2% exit /b 2 +if %a3% gtr %b3% exit /b 1 +if %a3% lss %b3% exit /b 2 +exit /b 0 + +:askinstall +cls +echo ### ### +echo # Python Not Found # +echo ### ### +echo. +echo Python !targetpy! was not found on the system or in the PATH var. +echo. +set /p "menu=Would you like to install it now? [y/n]: " +if /i "!menu!"=="y" ( + REM We got the OK - install it + goto installpy +) else if "!menu!"=="n" ( + REM No OK here... + set /a tried=!tried!+1 + goto checkpy +) +REM Incorrect answer - go back +goto askinstall + +:installpy +REM This will attempt to download and install python +REM First we get the html for the python downloads page for Windows +set /a tried=!tried!+1 +cls +echo ### ### +echo # Installing Python # +echo ### ### +echo. +echo Gathering info from https://www.python.org/downloads/windows/... +powershell -command "[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12;(new-object System.Net.WebClient).DownloadFile('https://www.python.org/downloads/windows/','%TEMP%\pyurl.txt')" +REM Extract it if it's gzip compressed +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{}" +if not exist "%TEMP%\pyurl.txt" ( + if /i "!just_installing!" == "TRUE" ( + echo Failed to get info + exit /b 1 + ) else ( + goto checkpy + ) +) +echo Parsing for latest... +pushd "%TEMP%" +:: Version detection code slimmed by LussacZheng (https://github.com/corpnewt/gibMacOS/issues/20) +for /f "tokens=9 delims=< " %%x in ('findstr /i /c:"Latest Python !targetpy! Release" pyurl.txt') do ( set "release=%%x" ) +popd +if "!release!" == "" ( + if /i "!just_installing!" == "TRUE" ( + echo Failed to get python version + exit /b 1 + ) else ( + goto checkpy + ) +) +echo Found Python !release! - Downloading... +REM Let's delete our txt file now - we no longer need it +del "%TEMP%\pyurl.txt" +REM At this point - we should have the version number. +REM We can build the url like so: "https://www.python.org/ftp/python/[version]/python-[version]-amd64.exe" +set "url=https://www.python.org/ftp/python/!release!/python-!release!-amd64.exe" +set "pytype=exe" +if "!targetpy!" == "2" ( + set "url=https://www.python.org/ftp/python/!release!/python-!release!.amd64.msi" + set "pytype=msi" +) +REM Now we download it with our slick powershell command +powershell -command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (new-object System.Net.WebClient).DownloadFile('!url!','%TEMP%\pyinstall.!pytype!')" +REM If it doesn't exist - we bail +if not exist "%TEMP%\pyinstall.!pytype!" ( + if /i "!just_installing!" == "TRUE" ( + echo Failed to download installer + exit /b 1 + ) else ( + goto checkpy + ) +) +REM It should exist at this point - let's run it to install silently +echo Installing... +pushd "%TEMP%" +if /i "!pytype!" == "exe" ( + echo pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 + pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 +) else ( + set "foldername=!release:.=!" + echo msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR="%LocalAppData%\Programs\Python\Python!foldername:~0,2!" + msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR="%LocalAppData%\Programs\Python\Python!foldername:~0,2!" +) +popd +echo Installer finished with %ERRORLEVEL% status. +REM Now we should be able to delete the installer and check for py again +del "%TEMP%\pyinstall.!pytype!" +REM If it worked, then we should have python in our PATH +REM this does not get updated right away though - let's try +REM manually updating the local PATH var +call :updatepath +if /i "!just_installing!" == "TRUE" ( + echo. + echo Done. +) else ( + goto checkpy +) +exit /b + +:runscript +REM Python found +cls +set "args=%*" +set "args=!args:"=!" +if "!args!"=="" ( + "!pypath!" "!thisDir!!script_name!" +) else ( + "!pypath!" "!thisDir!!script_name!" %* +) +if /i "!pause_on_error!" == "yes" ( + if not "%ERRORLEVEL%" == "0" ( + echo. + echo Script exited with error code: %ERRORLEVEL% + echo. + echo Press [enter] to exit... + pause > nul + ) +) +goto :EOF + +:undouble +REM Helper function to strip doubles of a single character out of a string recursively +set "string_value=%~2" +:undouble_continue +set "check=!string_value:%~3%~3=%~3!" +if not "!check!" == "!string_value!" ( + set "string_value=!check!" + goto :undouble_continue +) +set "%~1=!check!" +goto :EOF + +:updatepath +set "spath=" +set "upath=" +for /f "USEBACKQ tokens=2* delims= " %%i in (`!syspath!reg.exe query "HKCU\Environment" /v "Path" 2^> nul`) do ( if not "%%j" == "" set "upath=%%j" ) +for /f "USEBACKQ tokens=2* delims= " %%i in (`!syspath!reg.exe query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "Path" 2^> nul`) do ( if not "%%j" == "" set "spath=%%j" ) +if not "%spath%" == "" ( + REM We got something in the system path + set "PATH=%spath%" + if not "%upath%" == "" ( + REM We also have something in the user path + set "PATH=%PATH%;%upath%" + ) +) else if not "%upath%" == "" ( + set "PATH=%upath%" +) +REM Remove double semicolons from the adjusted PATH +call :undouble "PATH" "%PATH%" ";" +goto :EOF + +:getsyspath +REM Helper method to return a valid path to cmd.exe, reg.exe, and where.exe by +REM walking the ComSpec var - will also repair it in memory if need be +REM Strip double semi-colons +call :undouble "temppath" "%ComSpec%" ";" + +REM Dirty hack to leverage the "line feed" approach - there are some odd side +REM effects with this. Do not use this variable name in comments near this +REM line - as it seems to behave erradically. +(set LF=^ +%=this line is empty=% +) +REM Replace instances of semi-colons with a line feed and wrap +REM in parenthesis to work around some strange batch behavior +set "testpath=%temppath:;=!LF!%" + +REM Let's walk each path and test if cmd.exe, reg.exe, and where.exe exist there +set /a found=0 +for /f "tokens=* delims=" %%i in ("!testpath!") do ( + REM Only continue if we haven't found it yet + if not "%%i" == "" ( + if !found! lss 1 ( + set "checkpath=%%i" + REM Remove "cmd.exe" from the end if it exists + if /i "!checkpath:~-7!" == "cmd.exe" ( + set "checkpath=!checkpath:~0,-7!" + ) + REM Pad the end with a backslash if needed + if not "!checkpath:~-1!" == "\" ( + set "checkpath=!checkpath!\" + ) + REM Let's see if cmd, reg, and where exist there - and set it if so + if EXIST "!checkpath!cmd.exe" ( + if EXIST "!checkpath!reg.exe" ( + if EXIST "!checkpath!where.exe" ( + set /a found=1 + set "ComSpec=!checkpath!cmd.exe" + set "%~1=!checkpath!" + ) + ) + ) + ) + ) +) +goto :EOF diff --git a/brigadier.command b/brigadier.command new file mode 100755 index 0000000..09927e7 --- /dev/null +++ b/brigadier.command @@ -0,0 +1,332 @@ +#!/usr/bin/env bash + +# Get the curent directory, the script name +# and the script name with "py" substituted for the extension. +args=( "$@" ) +dir="$(cd -- "$(dirname "$0")" >/dev/null 2>&1; pwd -P)" +script="${0##*/}" +target="${script%.*}.py" + +# use_py3: +# TRUE = Use if found, use py2 otherwise +# FALSE = Use py2 +# FORCE = Use py3 +use_py3="TRUE" + +# We'll parse if the first argument passed is +# --install-python and if so, we'll just install +just_installing="FALSE" + +tempdir="" + +compare_to_version () { + # Compares our OS version to the passed OS version, and + # return a 1 if we match the passed compare type, or a 0 if we don't. + # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal) + # $2 = OS version to compare ours to + if [ -z "$1" ] || [ -z "$2" ]; then + # Missing info - bail. + return + fi + local current_os= comp= + current_os="$(sw_vers -productVersion)" + comp="$(vercomp "$current_os" "$2")" + # Check gequal and lequal first + if [[ "$1" == "3" && ("$comp" == "1" || "$comp" == "0") ]] || [[ "$1" == "4" && ("$comp" == "2" || "$comp" == "0") ]] || [[ "$comp" == "$1" ]]; then + # Matched + echo "1" + else + # No match + echo "0" + fi +} + +set_use_py3_if () { + # Auto sets the "use_py3" variable based on + # conditions passed + # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal) + # $2 = OS version to compare + # $3 = TRUE/FALSE/FORCE in case of match + if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then + # Missing vars - bail with no changes. + return + fi + if [ "$(compare_to_version "$1" "$2")" == "1" ]; then + use_py3="$3" + fi +} + +get_remote_py_version () { + local pyurl= py_html= py_vers= py_num="3" + pyurl="https://www.python.org/downloads/macos/" + py_html="$(curl -L $pyurl --compressed 2>&1)" + if [ -z "$use_py3" ]; then + use_py3="TRUE" + fi + if [ "$use_py3" == "FALSE" ]; then + py_num="2" + fi + py_vers="$(echo "$py_html" | grep -i "Latest Python $py_num Release" | awk '{print $8}' | cut -d'<' -f1)" + echo "$py_vers" +} + +download_py () { + local vers="$1" url= + clear + echo " ### ###" + echo " # Downloading Python #" + echo "### ###" + echo + if [ -z "$vers" ]; then + echo "Gathering latest version..." + vers="$(get_remote_py_version)" + fi + if [ -z "$vers" ]; then + # Didn't get it still - bail + print_error + fi + echo "Located Version: $vers" + echo + echo "Building download url..." + url="$(curl -L https://www.python.org/downloads/release/python-${vers//./}/ --compressed 2>&1 | grep -iE "python-$vers-macos.*.pkg\"" | awk -F'"' '{ print $2 }')" + if [ -z "$url" ]; then + # Couldn't get the URL - bail + print_error + fi + echo " - $url" + echo + echo "Downloading..." + echo + # Create a temp dir and download to it + tempdir="$(mktemp -d 2>/dev/null || mktemp -d -t 'tempdir')" + curl "$url" -o "$tempdir/python.pkg" + if [ "$?" != "0" ]; then + echo + echo " - Failed to download python installer!" + echo + exit $? + fi + echo + echo "Running python install package..." + echo + sudo installer -pkg "$tempdir/python.pkg" -target / + if [ "$?" != "0" ]; then + echo + echo " - Failed to install python!" + echo + exit $? + fi + # Now we expand the package and look for a shell update script + pkgutil --expand "$tempdir/python.pkg" "$tempdir/python" + if [ -e "$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall" ]; then + # Run the script + echo + echo "Updating PATH..." + echo + "$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall" + fi + vers_folder="Python $(echo "$vers" | cut -d'.' -f1 -f2)" + if [ -f "/Applications/$vers_folder/Install Certificates.command" ]; then + # Certs script exists - let's execute that to make sure our certificates are updated + echo + echo "Updating Certificates..." + echo + "/Applications/$vers_folder/Install Certificates.command" + fi + echo + echo "Cleaning up..." + cleanup + echo + if [ "$just_installing" == "TRUE" ]; then + echo "Done." + else + # Now we check for py again + echo "Rechecking py..." + downloaded="TRUE" + clear + main + fi +} + +cleanup () { + if [ -d "$tempdir" ]; then + rm -Rf "$tempdir" + fi +} + +print_error() { + clear + cleanup + echo " ### ###" + echo " # Python Not Found #" + echo "### ###" + echo + echo "Python is not installed or not found in your PATH var." + echo + if [ "$kernel" == "Darwin" ]; then + echo "Please go to https://www.python.org/downloads/macos/ to" + echo "download and install the latest version, then try again." + else + echo "Please install python through your package manager and" + echo "try again." + fi + echo + exit 1 +} + +print_target_missing() { + clear + cleanup + echo " ### ###" + echo " # Target Not Found #" + echo "### ###" + echo + echo "Could not locate $target!" + echo + exit 1 +} + +format_version () { + local vers="$1" + echo "$(echo "$1" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }')" +} + +vercomp () { + # Modified from: https://apple.stackexchange.com/a/123408/11374 + local ver1="$(format_version "$1")" ver2="$(format_version "$2")" + if [ $ver1 -gt $ver2 ]; then + echo "1" + elif [ $ver1 -lt $ver2 ]; then + echo "2" + else + echo "0" + fi +} + +get_local_python_version() { + # $1 = Python bin name (defaults to python3) + # Echoes the path to the highest version of the passed python bin if any + local py_name="$1" max_version= python= python_version= python_path= + if [ -z "$py_name" ]; then + py_name="python3" + fi + py_list="$(which -a "$py_name" 2>/dev/null)" + # Walk that newline separated list + while read python; do + if [ -z "$python" ]; then + # Got a blank line - skip + continue + fi + if [ "$check_py3_stub" == "1" ] && [ "$python" == "/usr/bin/python3" ]; then + # See if we have a valid developer path + xcode-select -p > /dev/null 2>&1 + if [ "$?" != "0" ]; then + # /usr/bin/python3 path - but no valid developer dir + continue + fi + fi + python_version="$(get_python_version $python)" + if [ -z "$python_version" ]; then + # Didn't find a py version - skip + continue + fi + # Got the py version - compare to our max + if [ -z "$max_version" ] || [ "$(vercomp "$python_version" "$max_version")" == "1" ]; then + # Max not set, or less than the current - update it + max_version="$python_version" + python_path="$python" + fi + done <<< "$py_list" + echo "$python_path" +} + +get_python_version() { + local py_path="$1" py_version= + # Get the python version by piping stderr into stdout (for py2), then grepping the output for + # the word "python", getting the second element, and grepping for an alphanumeric version number + py_version="$($py_path -V 2>&1 | grep -i python | cut -d' ' -f2 | grep -E "[A-Za-z\d\.]+")" + if [ ! -z "$py_version" ]; then + echo "$py_version" + fi +} + +prompt_and_download() { + if [ "$downloaded" != "FALSE" ] || [ "$kernel" != "Darwin" ]; then + # We already tried to download, or we're not on macOS - just bail + print_error + fi + clear + echo " ### ###" + echo " # Python Not Found #" + echo "### ###" + echo + target_py="Python 3" + printed_py="Python 2 or 3" + if [ "$use_py3" == "FORCE" ]; then + printed_py="Python 3" + elif [ "$use_py3" == "FALSE" ]; then + target_py="Python 2" + printed_py="Python 2" + fi + echo "Could not locate $printed_py!" + echo + echo "This script requires $printed_py to run." + echo + while true; do + read -p "Would you like to install the latest $target_py now? (y/n): " yn + case $yn in + [Yy]* ) download_py;break;; + [Nn]* ) print_error;; + esac + done +} + +main() { + local python= version= + # Verify our target exists + if [ ! -f "$dir/$target" ]; then + # Doesn't exist + print_target_missing + fi + if [ -z "$use_py3" ]; then + use_py3="TRUE" + fi + if [ "$use_py3" != "FALSE" ]; then + # Check for py3 first + python="$(get_local_python_version python3)" + fi + if [ "$use_py3" != "FORCE" ] && [ -z "$python" ]; then + # We aren't using py3 explicitly, and we don't already have a path + python="$(get_local_python_version python2)" + if [ -z "$python" ]; then + # Try just looking for "python" + python="$(get_local_python_version python)" + fi + fi + if [ -z "$python" ]; then + # Didn't ever find it - prompt + prompt_and_download + return 1 + fi + # Found it - start our script and pass all args + "$python" "$dir/$target" "${args[@]}" +} + +# Keep track of whether or not we're on macOS to determine if +# we can download and install python for the user as needed. +kernel="$(uname -s)" +# Check to see if we need to force based on +# macOS version. 10.15 has a dummy python3 version +# that can trip up some py3 detection in other scripts. +# set_use_py3_if "3" "10.15" "FORCE" +downloaded="FALSE" +# Check for the aforementioned /usr/bin/python3 stub if +# our OS version is 10.15 or greater. +check_py3_stub="$(compare_to_version "3" "10.15")" +trap cleanup EXIT +if [ "$1" == "--install-python" ] && [ "$kernel" == "Darwin" ]; then + just_installing="TRUE" + download_py +else + main +fi diff --git a/brigadier b/brigadier.py similarity index 59% rename from brigadier rename to brigadier.py index 90f2d28..13318dd 100755 --- a/brigadier +++ b/brigadier.py @@ -1,38 +1,89 @@ -#!/usr/bin/python - -import os -import sys -import subprocess -import urllib2 -import plistlib -import re -import tempfile -import shutil -import optparse -import datetime -import platform - -from pprint import pprint -from urllib import urlretrieve +import os,sys,subprocess,re,tempfile,shutil,optparse,datetime,platform,plistlib,json + from xml.dom import minidom +import downloader + +if 2/3==0: input = raw_input + +# SUCATALOG_URL = 'http://swscan.apple.com/content/catalogs/others/index-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog' +SUCATALOG_URL = 'https://swscan.apple.com/content/catalogs/others/index-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog' +if os.name=="nt": # Set Windows-specific properties + z_json = "https://sourceforge.net/projects/sevenzip/best_release.json" + z_url2 = "https://www.7-zip.org/a/7z1806-x64.msi" + z_url = "https://www.7-zip.org/a/7z[[vers]]-x64.msi" + z_name = "7z.exe" + z_path = None + z_path64 = os.path.join(os.environ['SYSTEMDRIVE'] + "\\", "Program Files", "7-Zip", "7z.exe") + z_path32 = os.path.join(os.environ['SYSTEMDRIVE'] + "\\", "Program Files (x86)", "7-Zip", "7z.exe") -SUCATALOG_URL = 'http://swscan.apple.com/content/catalogs/others/index-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog' -# 7-Zip MSI (15.14) -SEVENZIP_URL = 'http://www.7-zip.org/a/7z1514-x64.msi' +d = downloader.Downloader() def status(msg): - print "%s\n" % msg + print("{}\n".format(msg)) def getCommandOutput(cmd): p = subprocess.Popen(cmd, stdout=subprocess.PIPE) out, err = p.communicate() return out +def load_plist(fp): + if sys.version_info >= (3,0): + return plistlib.load(fp) + else: + return plistlib.readPlist(fp) + +def loads_plist(s): + if sys.version_info >= (3,0): + return plistlib.loads(s) + else: + return plistlib.readPlistFromString(s) + +def check_7z(): + global z_path + z_path = next((x for x in (z_path64,z_path32) if os.path.exists(x)),None) + if z_path: return True + print("Couldn't locate {} - downloading...".format(z_name)) + # Didn't find it - let's do some stupid stuff + # First we get our json response - or rather, try to, then parse it + # looking for the current version + dl_url = None + try: + json_data = json.loads(d.get_string(z_json,progress=False)) + v_num = json_data.get("release",{}).get("filename","").split("/")[-1].lower().split("-")[0].replace("7z","").replace(".exe","") + if len(v_num): + dl_url = z_url.replace("[[vers]]",v_num) + except: + pass + if not dl_url: # Fall back on a known-good version + dl_url = z_url2 + temp = tempfile.mkdtemp() + dl_name = os.path.basename(dl_url) + dl_file = d.stream_to_file(dl_url, os.path.join(temp, dl_name)) + print("") # Move to the next line to avoid overwriting + print(dl_file) + if not dl_file: # Didn't download right + shutil.rmtree(temp,ignore_errors=True) + return False + print("") + print("Installing 7zip...") + retcode = subprocess.call(['msiexec', '/qn', '/i', os.path.join(temp, dl_name)]) + if retcode != 0: + print("{} returned an error code of {} - trying to run in interactive mode...".format(dl_name,retcode)) + retcode = subprocess.call(['msiexec', '/i', os.path.join(temp, dl_name)]) + if retcode != 0: + shutil.rmtree(temp,ignore_errors=True) + print("Error ({})".format(retcode)) + exit(1) + print("") + z_path = next((x for x in (z_path64,z_path32) if os.path.exists(x)),None) + return z_path and os.path.exists(z_path) + # Returns this machine's model identifier, using wmic on Windows, # system_profiler on OS X def getMachineModel(): if platform.system() == 'Windows': - rawxml = getCommandOutput(['wmic', 'computersystem', 'get', 'model', '/format:RAWXML']) + wmic = os.path.join(os.environ['SYSTEMDRIVE'] + "\\", "Windows", "System32", "wbem", "WMIC.exe") + rawxml = getCommandOutput([wmic, 'computersystem', 'get', 'model', '/format:RAWXML']) dom = minidom.parseString(rawxml) results = dom.getElementsByTagName("RESULTS") nodes = results[0].getElementsByTagName("CIM")[0].getElementsByTagName("INSTANCE")[0]\ @@ -40,44 +91,28 @@ def getMachineModel(): model = nodes[0].data elif platform.system() == 'Darwin': plistxml = getCommandOutput(['system_profiler', 'SPHardwareDataType', '-xml']) - plist = plistlib.readPlistFromString(plistxml) + plist = loads_plist(plistxml) model = plist[0]['_items'][0]['machine_model'] return model -def downloadFile(url, filename): - # http://stackoverflow.com/questions/13881092/ - # download-progressbar-for-python-3/13895723#13895723 - def reporthook(blocknum, blocksize, totalsize): - readsofar = blocknum * blocksize - if totalsize > 0: - percent = readsofar * 1e2 / totalsize - console_out = "\r%5.1f%% %*d / %d bytes" % ( - percent, len(str(totalsize)), readsofar, totalsize) - sys.stderr.write(console_out) - if readsofar >= totalsize: # near the end - sys.stderr.write("\n") - else: # total size is unknown - sys.stderr.write("read %d\n" % (readsofar,)) - - urlretrieve(url, filename, reporthook=reporthook) - def sevenzipExtract(arcfile, command='e', out_dir=None): - cmd = [os.path.join(os.environ['SYSTEMDRIVE'] + "\\", "Program Files", "7-Zip", "7z.exe")] + if not z_path and not check_7z(): exit(1) # Yikes - shouldn't happen + cmd = [z_path] cmd.append(command) if not out_dir: out_dir = os.path.dirname(arcfile) cmd.append("-o" + out_dir) cmd.append("-y") cmd.append(arcfile) - status("Calling 7-Zip command: %s" % ' '.join(cmd)) + status("Calling 7-Zip command: {}".format(' '.join(cmd))) retcode = subprocess.call(cmd) if retcode: - sys.exit("Command failure: %s exited %s." % (' '.join(cmd), retcode)) + sys.exit("Command failure: {} exited {}.".format(' '.join(cmd), retcode)) def postInstallConfig(): regdata = """Windows Registry Editor Version 5.00 -[HKEY_CURRENT_USER\Software\Apple Inc.\Apple Keyboard Support] +[HKEY_CURRENT_USER\\Software\\Apple Inc.\\Apple Keyboard Support] "FirstTimeRun"=dword:00000000""" handle, path = tempfile.mkstemp() fd = os.fdopen(handle, 'w') @@ -102,12 +137,12 @@ def findBootcampMSI(search_dir): def installBootcamp(msipath): logpath = os.path.abspath("/BootCamp_Install.log") cmd = ['cmd', '/c', 'msiexec', '/i', msipath, '/qb-', '/norestart', '/log', logpath] - status("Executing command: '%s'" % " ".join(cmd)) + status("Executing command: '{}'".format(" ".join(cmd))) subprocess.call(cmd) status("Install log output:") with open(logpath, 'r') as logfd: logdata = logfd.read() - print logdata.decode('utf-16') + print(logdata.decode('utf-16')) postInstallConfig() def main(): @@ -131,6 +166,8 @@ def main(): help="Specify an exact product ID to download (ie. '031-0787'), currently useful only for cases \ where a model has multiple BootCamp ESDs available and is not downloading the desired version \ according to the post date.") + o.add_option('-l', '--latest-version', action="store_true", + help="Gets whichever version is latest. This overrides model and product-id if specified.") opts, args = o.parse_args() if opts.install: @@ -141,16 +178,15 @@ def main(): if opts.output_dir: if not os.path.isdir(opts.output_dir): - sys.exit("Output directory %s that was specified doesn't exist!" % opts.output_dir) + sys.exit("Output directory {} that was specified doesn't exist!".format(opts.output_dir)) if not os.access(opts.output_dir, os.W_OK): - sys.exit("Output directory %s is not writable by this user!" % opts.output_dir) + sys.exit("Output directory {} is not writable by this user!".format(opts.output_dir)) output_dir = opts.output_dir else: output_dir = os.getcwd() if output_dir.endswith('ystem32') or '\\system32\\' in output_dir.lower(): output_dir = os.environ['SystemDrive'] + "\\" - status("Changing output directory to %s to work around an issue \ -when running the installer out of 'system32'." % output_dir) + status("Changing output directory to {} to work around an issue when running the installer out of 'system32'.".format(output_dir)) if opts.keep_files and not opts.install: sys.exit("The --keep-files option is only useful when used with --install option!") @@ -159,13 +195,23 @@ def main(): if opts.install: status("Ignoring '--model' when '--install' is used. The Boot Camp " "installer won't allow other models to be installed, anyway.") + if opts.latest_version: + status("Ignoring '--model' when '--latest-version' is used. The Boot Camp " + "installer won't allow other models to be installed, anyway.") models = opts.model else: models = [getMachineModel()] + if opts.latest_version: + if opts.install: + status("Ignoring '--latest-version' when '--install' is used. The Boot Camp " + "installer won't allow other models to be installed, anyway.") + opts.latest_version = False + else: + models = ["All"] if len(models) > 1: - status("Using Mac models: %s." % ', '.join(models)) + status("Using Mac models: {}.".format(', '.join(models))) else: - status("Using Mac model: %s." % ', '.join(models)) + status("Using Mac model: {}.".format(', '.join(models))) for model in models: sucatalog_url = SUCATALOG_URL @@ -174,118 +220,109 @@ def main(): plist_path = os.path.join(scriptdir, 'brigadier.plist') if os.path.isfile(plist_path): try: - config_plist = plistlib.readPlist(plist_path) + with open(plist_path, "rb") as f: + config_plist = load_plist(f) except: - status("Config plist was found at %s but it could not be read. \ - Verify that it is readable and is an XML formatted plist." % plist_path) + status("Config plist was found at {} but it could not be read. \ + Verify that it is readable and is an XML formatted plist.".format(plist_path)) if config_plist: - if 'CatalogURL' in config_plist.keys(): + if 'CatalogURL' in list(config_plist): sucatalog_url = config_plist['CatalogURL'] - urlfd = urllib2.urlopen(sucatalog_url) - data = urlfd.read() - p = plistlib.readPlistFromString(data) + data = d.get_bytes(sucatalog_url,progress=False) + p = loads_plist(data) allprods = p['Products'] # Get all Boot Camp ESD products bc_prods = [] for (prod_id, prod_data) in allprods.items(): - if 'ServerMetadataURL' in prod_data.keys(): + if 'ServerMetadataURL' in list(prod_data): bc_match = re.search('BootCamp', prod_data['ServerMetadataURL']) if bc_match: bc_prods.append((prod_id, prod_data)) # Find the ESD(s) that applies to our model pkg_data = [] - re_model = "([a-zA-Z]{4,12}[1-9]{1,2}\,[1-6])" + re_model = r"([a-zA-Z]{4,12}[1-9]{1,2}\,[1-6])" for bc_prod in bc_prods: - if 'English' in bc_prod[1]['Distributions'].keys(): + if 'English' in list(bc_prod[1]['Distributions']): disturl = bc_prod[1]['Distributions']['English'] - distfd = urllib2.urlopen(disturl) - dist_data = distfd.read() - if re.search(model, dist_data): + dist_data = d.get_string(disturl,progress=False) + if opts.latest_version or re.search(model, dist_data): supported_models = [] pkg_data.append({bc_prod[0]: bc_prod[1]}) model_matches_in_dist = re.findall(re_model, dist_data) for supported_model in model_matches_in_dist: supported_models.append(supported_model) - status("Model supported in package distribution file at %s." % disturl) - status("Distribution %s supports the following models: %s." % - (bc_prod[0], ", ".join(supported_models))) + status("Model supported in package distribution file at {}.".format(disturl)) + status("Distribution {} supports the following models: {}.".format( + bc_prod[0], ", ".join(supported_models))) # Ensure we have only one ESD if len(pkg_data) == 0: - sys.exit("Couldn't find a Boot Camp ESD for the model %s in the given software update catalog." % model) + sys.exit("Couldn't find a Boot Camp ESD for the model {} in the given software update catalog.".format(model)) if len(pkg_data) == 1: pkg_data = pkg_data[0] if opts.product_id: sys.exit("--product-id option is only applicable when multiple ESDs are found for a model.") if len(pkg_data) > 1: - # sys.exit("There is more than one ESD product available for this model: %s. " + # sys.exit("There is more than one ESD product available for this model: {}. " # "Automically selecting the one with the most recent PostDate.." - # % ", ".join([p.keys()[0] for p in pkg_data])) - print "There is more than one ESD product available for this model:" + # .format(", ".join([list(p)[0] for p in pkg_data])) + print("There is more than one ESD product available for this model:") # Init latest to be epoch start latest_date = datetime.datetime.fromtimestamp(0) chosen_product = None for i, p in enumerate(pkg_data): - product = p.keys()[0] + product = list(p)[0] postdate = p[product].get('PostDate') - print "%s: PostDate %s" % (product, postdate) + print("{}: PostDate {}".format(product, postdate)) if postdate > latest_date: latest_date = postdate chosen_product = product if opts.product_id: - if opts.product_id not in [k.keys()[0] for k in pkg_data]: - sys.exit("Product specified with '--product-id %s' either doesn't exist " - "or was not found applicable to models: %s" - % (opts.product_id, ", ".join(models))) + if opts.product_id not in [list(k)[0] for k in pkg_data]: + sys.exit("Product specified with '--product-id {}' either doesn't exist " + "or was not found applicable to models: {}" + .format(opts.product_id, ", ".join(models))) chosen_product = opts.product_id - print "Selecting manually-chosen product %s." % chosen_product + print("Selecting manually-chosen product {}.".format(chosen_product)) else: - print "Selecting %s as it's the most recently posted." % chosen_product + print("Selecting {} as it's the most recently posted.".format(chosen_product)) for p in pkg_data: - if p.keys()[0] == chosen_product: + if list(p)[0] == chosen_product: selected_pkg = p pkg_data = selected_pkg - pkg_id = pkg_data.keys()[0] - pkg_url = pkg_data.values()[0]['Packages'][0]['URL'] + pkg_id = list(pkg_data)[0] + pkg_url = pkg_data[pkg_id]['Packages'][0]['URL'] # make a sub-dir in the output_dir here, named by product landing_dir = os.path.join(output_dir, 'BootCamp-' + pkg_id) if os.path.exists(landing_dir): - status("Final output path %s already exists, removing it..." % landing_dir) + status("Final output path {} already exists, removing it...".format(landing_dir)) if platform.system() == 'Windows': # using rmdir /qs because shutil.rmtree dies on the Doc files with foreign language characters subprocess.call(['cmd', '/c', 'rmdir', '/q', '/s', landing_dir]) else: shutil.rmtree(landing_dir) - status("Making directory %s.." % landing_dir) + status("Making directory {}..".format(landing_dir)) os.mkdir(landing_dir) arc_workdir = tempfile.mkdtemp(prefix="bootcamp-unpack_") pkg_dl_path = os.path.join(arc_workdir, pkg_url.split('/')[-1]) - status("Fetching Boot Camp product at URL %s." % pkg_url) - downloadFile(pkg_url, pkg_dl_path) + status("Fetching Boot Camp product at URL {}.".format(pkg_url)) + d.stream_to_file(pkg_url, pkg_dl_path) + print("") # Move to the next line to avoid overwriting if platform.system() == 'Windows': - we_installed_7zip = False - sevenzip_binary = os.path.join(os.environ['SYSTEMDRIVE'] + "\\", 'Program Files', '7-Zip', '7z.exe') - # fetch and install 7-Zip - if not os.path.exists(sevenzip_binary): - tempdir = tempfile.mkdtemp() - sevenzip_msi_dl_path = os.path.join(tempdir, SEVENZIP_URL.split('/')[-1]) - downloadFile(SEVENZIP_URL, sevenzip_msi_dl_path) - status("Downloaded 7-zip to %s." % sevenzip_msi_dl_path) - status("We need to install 7-Zip..") - retcode = subprocess.call(['msiexec', '/qn', '/i', sevenzip_msi_dl_path]) - status("7-Zip install returned exit code %s." % retcode) - we_installed_7zip = True + if not check_7z(): + print("7-Zip was not found - aborting...") + exit(1) status("Extracting...") # BootCamp.pkg (xar) -> Payload (gzip) -> Payload~ (cpio) -> WindowsSupport.dmg @@ -298,9 +335,6 @@ def main(): sevenzipExtract(os.path.join(arc_workdir, 'WindowsSupport.dmg'), command='x', out_dir=landing_dir) - if we_installed_7zip: - status("Cleaning up the 7-Zip install...") - subprocess.call(['cmd', '/c', 'msiexec', '/qn', '/x', sevenzip_msi_dl_path]) if opts.install: status("Installing Boot Camp...") installBootcamp(findBootcampMSI(landing_dir)) @@ -320,20 +354,21 @@ def main(): output_file = os.path.join(landing_dir, 'WindowsSupport.dmg') shutil.move(os.path.join(arc_workdir, 'Library/Application Support/BootCamp/WindowsSupport.dmg'), output_file) - status("Extracted to %s." % output_file) + status("Extracted to {}.".format(output_file)) # If we were to also copy out the contents from the .dmg we might do it like this, but if you're doing this # from OS X you probably would rather just burn a disc so we'll stop here.. # mountxml = getCommandOutput(['/usr/bin/hdiutil', 'attach', # os.path.join(arc_workdir, 'Library/Application Support/BootCamp/WindowsSupport.dmg'), # '-mountrandom', '/tmp', '-plist', '-nobrowse']) - # mountplist = plistlib.readPlistFromString(mountxml) + # mountplist = loads_plist(mountxml) # mntpoint = mountplist['system-entities'][0]['mount-point'] # shutil.copytree(mntpoint, output_dir) # subprocess.call(['/usr/bin/hdiutil', 'eject', mntpoint]) shutil.rmtree(arc_workdir) status("Done.") + input("Press [enter] to exit...") if __name__ == "__main__": main() diff --git a/build_windows_exe.py b/build_windows_exe.py old mode 100644 new mode 100755 diff --git a/downloader.py b/downloader.py new file mode 100755 index 0000000..0fcaa63 --- /dev/null +++ b/downloader.py @@ -0,0 +1,287 @@ +import sys, os, time, ssl, gzip, multiprocessing +from io import BytesIO +# Python-aware urllib stuff +try: + from urllib.request import urlopen, Request + import queue as q +except ImportError: + # Import urllib2 to catch errors + import urllib2 + from urllib2 import urlopen, Request + import Queue as q + +TERMINAL_WIDTH = 120 if os.name=="nt" else 80 + +def get_size(size, suffix=None, use_1024=False, round_to=2, strip_zeroes=False): + # size is the number of bytes + # suffix is the target suffix to locate (B, KB, MB, etc) - if found + # use_2014 denotes whether or not we display in MiB vs MB + # round_to is the number of dedimal points to round our result to (0-15) + # strip_zeroes denotes whether we strip out zeroes + + # Failsafe in case our size is unknown + if size == -1: + return "Unknown" + # Get our suffixes based on use_1024 + ext = ["B","KiB","MiB","GiB","TiB","PiB"] if use_1024 else ["B","KB","MB","GB","TB","PB"] + div = 1024 if use_1024 else 1000 + s = float(size) + s_dict = {} # Initialize our dict + # Iterate the ext list, and divide by 1000 or 1024 each time to setup the dict {ext:val} + for e in ext: + s_dict[e] = s + s /= div + # Get our suffix if provided - will be set to None if not found, or if started as None + suffix = next((x for x in ext if x.lower() == suffix.lower()),None) if suffix else suffix + # Get the largest value that's still over 1 + biggest = suffix if suffix else next((x for x in ext[::-1] if s_dict[x] >= 1), "B") + # Determine our rounding approach - first make sure it's an int; default to 2 on error + try:round_to=int(round_to) + except:round_to=2 + round_to = 0 if round_to < 0 else 15 if round_to > 15 else round_to # Ensure it's between 0 and 15 + bval = round(s_dict[biggest], round_to) + # Split our number based on decimal points + a,b = str(bval).split(".") + # Check if we need to strip or pad zeroes + b = b.rstrip("0") if strip_zeroes else b.ljust(round_to,"0") if round_to > 0 else "" + return "{:,}{} {}".format(int(a),"" if not b else "."+b,biggest) + +def _process_hook(queue, total_size, update_interval=1.0, max_packets=0): + bytes_so_far = 0 + packets = [] + speed = remaining = "" + last_update = time.time() + while True: + # Write our info first so we have *some* status while + # waiting for packets + if total_size > 0: + percent = float(bytes_so_far) / total_size + percent = round(percent*100, 2) + t_s = get_size(total_size) + try: + b_s = get_size(bytes_so_far, t_s.split(" ")[1]) + except: + b_s = get_size(bytes_so_far) + perc_str = " {:.2f}%".format(percent) + bar_width = (TERMINAL_WIDTH // 3)-len(perc_str) + progress = "=" * int(bar_width * (percent/100)) + sys.stdout.write("\r\033[K{}/{} | {}{}{}{}{}".format( + b_s, + t_s, + progress, + " " * (bar_width-len(progress)), + perc_str, + speed, + remaining + )) + else: + b_s = get_size(bytes_so_far) + sys.stdout.write("\r\033[K{}{}".format(b_s, speed)) + sys.stdout.flush() + # Now we gather the next packet + try: + packet = queue.get(timeout=update_interval) + # Packets should be formatted as a tuple of + # (timestamp, len(bytes_downloaded)) + # If "DONE" is passed, we assume the download + # finished - and bail + if packet == "DONE": + print("") # Jump to the next line + return + # Append our packet to the list and ensure we're not + # beyond our max. + # Only check max if it's > 0 + packets.append(packet) + if max_packets > 0: + packets = packets[-max_packets:] + # Increment our bytes so far as well + bytes_so_far += packet[1] + except q.Empty: + # Didn't get anything - reset the speed + # and packets + packets = [] + speed = " | 0 B/s" + remaining = " | ?? left" if total_size > 0 else "" + except KeyboardInterrupt: + print("") # Jump to the next line + return + # If we have packets and it's time for an update, process + # the info. + update_check = time.time() + if packets and update_check - last_update >= update_interval: + last_update = update_check # Refresh our update timestamp + speed = " | ?? B/s" + if len(packets) > 1: + # Let's calculate the amount downloaded over how long + try: + first,last = packets[0][0],packets[-1][0] + chunks = sum([float(x[1]) for x in packets]) + t = last-first + assert t >= 0 + bytes_speed = 1. / t * chunks + speed = " | {}/s".format(get_size(bytes_speed,round_to=1)) + # Get our remaining time + if total_size > 0: + seconds_left = (total_size-bytes_so_far) / bytes_speed + days = seconds_left // 86400 + hours = (seconds_left - (days*86400)) // 3600 + mins = (seconds_left - (days*86400) - (hours*3600)) // 60 + secs = seconds_left - (days*86400) - (hours*3600) - (mins*60) + if days > 99 or bytes_speed == 0: + remaining = " | ?? left" + else: + remaining = " | {}{:02d}:{:02d}:{:02d} left".format( + "{}:".format(int(days)) if days else "", + int(hours), + int(mins), + int(round(secs)) + ) + except: + pass + # Clear the packets so we don't reuse the same ones + packets = [] + +class Downloader: + + def __init__(self,**kwargs): + self.ua = kwargs.get("useragent",{"User-Agent":"Mozilla"}) + self.chunk = 1048576 # 1024 x 1024 i.e. 1MiB + if os.name=="nt": os.system("color") # Initialize cmd for ANSI escapes + # Provide reasonable default logic to workaround macOS CA file handling + cafile = ssl.get_default_verify_paths().openssl_cafile + try: + # If default OpenSSL CA file does not exist, use that from certifi + if not os.path.exists(cafile): + import certifi + cafile = certifi.where() + self.ssl_context = ssl.create_default_context(cafile=cafile) + except: + # None of the above worked, disable certificate verification for now + self.ssl_context = ssl._create_unverified_context() + return + + def _decode(self, value, encoding="utf-8", errors="ignore"): + # Helper method to only decode if bytes type + if sys.version_info >= (3,0) and isinstance(value, bytes): + return value.decode(encoding,errors) + return value + + def _update_main_name(self): + # Windows running python 2 seems to have issues with multiprocessing + # if the case of the main script's name is incorrect: + # e.g. Downloader.py vs downloader.py + # + # To work around this, we try to scrape for the correct case if + # possible. + try: + path = os.path.abspath(sys.modules["__main__"].__file__) + except AttributeError as e: + # This likely means we're running from the interpreter + # directly + return None + if not os.path.isfile(path): + return None + # Get the file name and folder path + name = os.path.basename(path).lower() + fldr = os.path.dirname(path) + # Walk the files in the folder until we find our + # name - then steal its case and update that path + for f in os.listdir(fldr): + if f.lower() == name: + # Got it + new_path = os.path.join(fldr,f) + sys.modules["__main__"].__file__ = new_path + return new_path + # If we got here, it wasn't found + return None + + def open_url(self, url, headers = None): + # Fall back on the default ua if none provided + headers = self.ua if headers is None else headers + # Wrap up the try/except block so we don't have to do this for each function + try: + response = urlopen(Request(url, headers=headers), context=self.ssl_context) + except Exception as e: + # No fixing this - bail + return None + return response + + def get_size(self, *args, **kwargs): + return get_size(*args,**kwargs) + + def get_string(self, url, progress = True, headers = None, expand_gzip = True): + response = self.get_bytes(url,progress,headers,expand_gzip) + if response is None: return None + return self._decode(response) + + def get_bytes(self, url, progress = True, headers = None, expand_gzip = True): + response = self.open_url(url, headers) + if response is None: return None + try: total_size = int(response.headers['Content-Length']) + except: total_size = -1 + chunk_so_far = b"" + packets = queue = process = None + if progress: + # Make sure our vars are initialized + packets = [] if progress else None + queue = multiprocessing.Queue() + # Create the multiprocess and start it + process = multiprocessing.Process(target=_process_hook,args=(queue,total_size)) + process.daemon = True + # Filthy hack for earlier python versions on Windows + if os.name == "nt" and hasattr(multiprocessing,"forking"): + self._update_main_name() + process.start() + while True: + chunk = response.read(self.chunk) + if progress: + # Add our items to the queue + queue.put((time.time(),len(chunk))) + if not chunk: break + chunk_so_far += chunk + if expand_gzip and response.headers.get("Content-Encoding","unknown").lower() == "gzip": + fileobj = BytesIO(chunk_so_far) + gfile = gzip.GzipFile(fileobj=fileobj) + return gfile.read() + if progress: + # Finalize the queue and wait + queue.put("DONE") + process.join() + return chunk_so_far + + def stream_to_file(self, url, file_path, progress = True, headers = None, ensure_size_if_present = True): + response = self.open_url(url, headers) + if response is None: return None + bytes_so_far = 0 + try: total_size = int(response.headers['Content-Length']) + except: total_size = -1 + packets = queue = process = None + if progress: + # Make sure our vars are initialized + packets = [] if progress else None + queue = multiprocessing.Queue() + # Create the multiprocess and start it + process = multiprocessing.Process(target=_process_hook,args=(queue,total_size)) + process.daemon = True + # Filthy hack for earlier python versions on Windows + if os.name == "nt" and hasattr(multiprocessing,"forking"): + self._update_main_name() + process.start() + with open(file_path, 'wb') as f: + while True: + chunk = response.read(self.chunk) + bytes_so_far += len(chunk) + if progress: + # Add our items to the queue + queue.put((time.time(),len(chunk))) + if not chunk: break + f.write(chunk) + if progress: + # Finalize the queue and wait + queue.put("DONE") + process.join() + if ensure_size_if_present and total_size != -1: + # We're verifying size - make sure we got what we asked for + if bytes_so_far != total_size: + return None # We didn't - imply it failed + return file_path if os.path.exists(file_path) else None diff --git a/msi-transform.bat b/msi-transform.bat new file mode 100644 index 0000000..9152a70 --- /dev/null +++ b/msi-transform.bat @@ -0,0 +1,60 @@ +@echo off +setlocal enableDelayedExpansion + +REM Setup initial vars +set "script_path=msi-transform" +set "script_name=WiUseXfm.vbs" +set "mst_file=set_nocheck.mst" +set "bc_path=BootCamp\Drivers\Apple\BootCamp.msi" +set "this_dir=%~dp0" +set "script=!this_dir!\!script_path!\!script_name!" +set "mst=!this_dir!\!script_path!\!mst_file!" + +cls +echo ### ### +echo # MSI Transform # +echo ### ### +echo. +if "%~1"=="" ( + echo No file given. You must drop the Boot Camp folder onto this script. + echo. + echo Press [enter] to exit... + pause > nul + exit /b +) +echo Got "%~1" +if not exist "%~1\!bc_path!" ( + echo Could not locate "!bc_path!" in the dropped folder. + echo. + echo Press [enter] to exit... + pause > nul + exit /b +) +echo Located BootCamp.msi +if not exist "!script!" ( + echo Could not locate "!script!". + echo. + echo Press [enter] to exit... + pause > nul + exit /b +) +if not exist "!mst!" ( + echo Could not locate "!mst!". + echo. + echo Press [enter] to exit... + pause > nul + exit /b +) +echo Located script and mst. +echo Applying changes... +echo "%WINDIR%\System32\cscript.exe" "!script!" "%~1\!bc_path!" "!mst!" +"%WINDIR%\System32\cscript.exe" "!script!" "%~1\!bc_path!" "!mst!" +echo. +echo Launching with admin privs... +echo. +powershell -Command "Start-Process cmd -Verb RunAs -ArgumentList '/c """%~1\!bc_path!"""'" +echo Done. +echo. +echo Press [enter] to exit... +pause > nul +exit /b \ No newline at end of file