This commit is contained in:
Timothy Sutton 2021-11-07 17:10:38 -05:00 committed by GitHub
commit ee33f3b8bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 257 additions and 138 deletions

14
.gitignore vendored
View file

@ -1,6 +1,12 @@
.DS_Store
pyinstaller.zip
pyinstaller-*
brigadier*.zip
/brigadier.plist
# virtualenv
.venv
# possible brigadier outputs
BootCamp-*
# pyinstaller
build
dist
*.spec

373
brigadier
View file

@ -1,49 +1,58 @@
#!/usr/bin/python
#!/usr/bin/env python3
import datetime
import optparse
import os
import sys
import subprocess
import urllib2
import platform
import plistlib
import re
import tempfile
import shutil
import optparse
import datetime
import platform
from pprint import pprint
from urllib import urlretrieve
import subprocess
import sys
import tempfile
from urllib.request import urlopen, urlretrieve
from xml.dom import minidom
SUCATALOG_URL = 'https://swscan.apple.com/content/catalogs/others/index-11-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog'
SUCATALOG_URL = "https://swscan.apple.com/content/catalogs/others/index-11-10.15-10.14-10.13-10.12-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'
SEVENZIP_URL = "http://www.7-zip.org/a/7z1514-x64.msi"
def status(msg):
print "%s\n" % msg
print("%s\n" % msg)
def getCommandOutput(cmd):
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
out, err = p.communicate()
return out
# 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'])
if platform.system() == "Windows":
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]\
.getElementsByTagName("PROPERTY")[0].getElementsByTagName("VALUE")[0].childNodes
nodes = (
results[0]
.getElementsByTagName("CIM")[0]
.getElementsByTagName("INSTANCE")[0]
.getElementsByTagName("PROPERTY")[0]
.getElementsByTagName("VALUE")[0]
.childNodes
)
model = nodes[0].data
elif platform.system() == 'Darwin':
plistxml = getCommandOutput(['system_profiler', 'SPHardwareDataType', '-xml'])
plist = plistlib.readPlistFromString(plistxml)
model = plist[0]['_items'][0]['machine_model']
elif platform.system() == "Darwin":
plistxml = getCommandOutput(["system_profiler", "SPHardwareDataType", "-xml"])
plist = plistlib.loads(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
@ -52,27 +61,37 @@ def downloadFile(url, filename):
if totalsize > 0:
percent = readsofar * 1e2 / totalsize
console_out = "\r%5.1f%% %*d / %d bytes" % (
percent, len(str(totalsize)), readsofar, totalsize)
percent,
len(str(totalsize)),
readsofar,
totalsize,
)
sys.stderr.write(console_out)
if readsofar >= totalsize: # near the end
if readsofar >= totalsize: # near the end
sys.stderr.write("\n")
else: # total size is unknown
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")]
def sevenzipExtract(arcfile, command="e", out_dir=None):
cmd = [
os.path.join(
os.environ["SYSTEMDRIVE"] + "\\", "Program Files", "7-Zip", "7z.exe"
)
]
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: %s" % " ".join(cmd))
retcode = subprocess.call(cmd)
if retcode:
sys.exit("Command failure: %s exited %s." % (' '.join(cmd), retcode))
sys.exit("Command failure: %s exited %s." % (" ".join(cmd), retcode))
def postInstallConfig():
regdata = """Windows Registry Editor Version 5.00
@ -80,10 +99,11 @@ def postInstallConfig():
[HKEY_CURRENT_USER\Software\Apple Inc.\Apple Keyboard Support]
"FirstTimeRun"=dword:00000000"""
handle, path = tempfile.mkstemp()
fd = os.fdopen(handle, 'w')
fd = os.fdopen(handle, "w")
fd.write(regdata)
fd.close()
subprocess.call(['regedit.exe', '/s', path])
subprocess.call(["regedit.exe", "/s", path])
def findBootcampMSI(search_dir):
"""Returns the path of the 64-bit BootCamp MSI"""
@ -93,174 +113,221 @@ def findBootcampMSI(search_dir):
# it's an AutoUnattend-based ESD as well, ie:
# /Drivers/Apple/BootCamp64.msi, or
# /BootCamp/Drivers/Apple/BootCamp.msi
candidates = ['BootCamp64.msi', 'BootCamp.msi']
candidates = ["BootCamp64.msi", "BootCamp.msi"]
for root, dirs, files in os.walk(search_dir):
for msi in candidates:
if msi in files:
return os.path.join(root, msi)
def installBootcamp(msipath):
logpath = os.path.abspath("/BootCamp_Install.log")
cmd = ['cmd', '/c', 'msiexec', '/i', msipath, '/qb-', '/norestart', '/log', logpath]
cmd = ["cmd", "/c", "msiexec", "/i", msipath, "/qb-", "/norestart", "/log", logpath]
status("Executing command: '%s'" % " ".join(cmd))
subprocess.call(cmd)
status("Install log output:")
with open(logpath, 'r') as logfd:
with open(logpath, "r") as logfd:
logdata = logfd.read()
print logdata.decode('utf-16')
print(logdata.decode("utf-16"))
postInstallConfig()
def main():
scriptdir = os.path.abspath(os.path.dirname(sys.argv[0]))
o = optparse.OptionParser()
o.add_option('-m', '--model', action="append",
o.add_option(
"-m",
"--model",
action="append",
help="System model identifier to use (otherwise this machine's \
model is used). This can be specified multiple times to download \
multiple models in a single run.")
o.add_option('-i', '--install', action="store_true",
multiple models in a single run.",
)
o.add_option(
"-i",
"--install",
action="store_true",
help="After the installer is downloaded, perform the install automatically. \
Can be used on Windows only.")
o.add_option('-o', '--output-dir',
Can be used on Windows only.",
)
o.add_option(
"-o",
"--output-dir",
help="Base path where the installer files will be extracted into a folder named after the \
product, ie. 'BootCamp-041-1234'. Uses the current directory if this option is omitted.")
o.add_option('-k', '--keep-files', action="store_true",
product, ie. 'BootCamp-041-1234'. Uses the current directory if this option is omitted.",
)
o.add_option(
"-k",
"--keep-files",
action="store_true",
help="Keep the files that were downloaded/extracted. Useful only with the \
'--install' option on Windows.")
o.add_option('-p', '--product-id',
'--install' option on Windows.",
)
o.add_option(
"-p",
"--product-id",
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.")
according to the post date.",
)
opts, args = o.parse_args()
if opts.install:
if platform.system() == 'Darwin':
if platform.system() == "Darwin":
sys.exit("Installing Boot Camp can only be done on Windows!")
if platform.system() == 'Windows' and platform.machine() != 'AMD64':
sys.exit("Installing on anything other than 64-bit Windows is currently not supported!")
if platform.system() == "Windows" and platform.machine() != "AMD64":
sys.exit(
"Installing on anything other than 64-bit Windows is currently not supported!"
)
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 %s that was specified doesn't exist!"
% 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 %s is not writable by this user!" % 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)
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
)
if opts.keep_files and not opts.install:
sys.exit("The --keep-files option is only useful when used with --install option!")
sys.exit(
"The --keep-files option is only useful when used with --install option!"
)
if opts.model:
if opts.install:
status("Ignoring '--model' when '--install' is used. The Boot Camp "
"installer won't allow other models to be installed, anyway.")
status(
"Ignoring '--model' when '--install' is used. The Boot Camp "
"installer won't allow other models to be installed, anyway."
)
models = opts.model
else:
models = [getMachineModel()]
if len(models) > 1:
status("Using Mac models: %s." % ', '.join(models))
status("Using Mac models: %s." % ", ".join(models))
else:
status("Using Mac model: %s." % ', '.join(models))
status("Using Mac model: %s." % ", ".join(models))
for model in models:
sucatalog_url = SUCATALOG_URL
# check if we defined anything in brigadier.plist
config_plist = None
plist_path = os.path.join(scriptdir, 'brigadier.plist')
plist_path = os.path.join(scriptdir, "brigadier.plist")
if os.path.isfile(plist_path):
try:
config_plist = plistlib.readPlist(plist_path)
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 %s but it could not be read. \
Verify that it is readable and is an XML formatted plist."
% plist_path
)
if config_plist:
if 'CatalogURL' in config_plist.keys():
sucatalog_url = config_plist['CatalogURL']
if "CatalogURL" in config_plist.keys():
sucatalog_url = config_plist["CatalogURL"]
urlfd = urllib2.urlopen(sucatalog_url)
urlfd = urlopen(sucatalog_url)
data = urlfd.read()
p = plistlib.readPlistFromString(data)
allprods = p['Products']
p = plistlib.loads(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():
bc_match = re.search('BootCamp', prod_data['ServerMetadataURL'])
if "ServerMetadataURL" in prod_data.keys():
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])"
for bc_prod in bc_prods:
if 'English' in bc_prod[1]['Distributions'].keys():
disturl = bc_prod[1]['Distributions']['English']
distfd = urllib2.urlopen(disturl)
dist_data = distfd.read()
if "English" in bc_prod[1]["Distributions"].keys():
disturl = bc_prod[1]["Distributions"]["English"]
distfd = urlopen(disturl)
dist_data = distfd.read().decode("utf-8")
if 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 %s." % disturl
)
status(
"Distribution %s supports the following models: %s."
% (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 %s in the given software update catalog."
% 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.")
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. "
# "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:"
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]
postdate = p[product].get('PostDate')
print "%s: PostDate %s" % (product, postdate)
product = list(p)[0]
postdate = p[product].get("PostDate")
print("%s: PostDate %s" % (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 %s' either doesn't exist "
"or was not found applicable to models: %s"
% (opts.product_id, ", ".join(models))
)
chosen_product = opts.product_id
print "Selecting manually-chosen product %s." % chosen_product
print("Selecting manually-chosen product %s." % chosen_product)
else:
print "Selecting %s as it's the most recently posted." % chosen_product
print("Selecting %s as it's the most recently posted." % 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 = list(pkg_data.values())[0]["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)
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)
if platform.system() == 'Windows':
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])
subprocess.call(["cmd", "/c", "rmdir", "/q", "/s", landing_dir])
else:
shutil.rmtree(landing_dir)
@ -268,58 +335,89 @@ when running the installer out of 'system32'." % output_dir)
os.mkdir(landing_dir)
arc_workdir = tempfile.mkdtemp(prefix="bootcamp-unpack_")
pkg_dl_path = os.path.join(arc_workdir, pkg_url.split('/')[-1])
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)
if platform.system() == 'Windows':
if platform.system() == "Windows":
we_installed_7zip = False
sevenzip_binary = os.path.join(os.environ['SYSTEMDRIVE'] + "\\", 'Program Files', '7-Zip', '7z.exe')
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])
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])
retcode = subprocess.call(
["msiexec", "/qn", "/i", sevenzip_msi_dl_path]
)
status("7-Zip install returned exit code %s." % retcode)
we_installed_7zip = True
status("Extracting...")
# BootCamp.pkg (xar) -> Payload (gzip) -> Payload~ (cpio) -> WindowsSupport.dmg
for arc in [pkg_dl_path,
os.path.join(arc_workdir, 'Payload'),
os.path.join(arc_workdir, 'Payload~')]:
for arc in [
pkg_dl_path,
os.path.join(arc_workdir, "Payload"),
os.path.join(arc_workdir, "Payload~"),
]:
if os.path.exists(arc):
sevenzipExtract(arc)
# finally, 7-Zip also extracts the tree within the DMG to the output dir
sevenzipExtract(os.path.join(arc_workdir, 'WindowsSupport.dmg'),
command='x',
out_dir=landing_dir)
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])
subprocess.call(
["cmd", "/c", "msiexec", "/qn", "/x", sevenzip_msi_dl_path]
)
if opts.install:
status("Installing Boot Camp...")
installBootcamp(findBootcampMSI(landing_dir))
if not opts.keep_files:
subprocess.call(['cmd', '/c', 'rmdir', '/q', '/s', landing_dir])
subprocess.call(["cmd", "/c", "rmdir", "/q", "/s", landing_dir])
# clean up the temp dir always
subprocess.call(['cmd', '/c', 'rmdir', '/q', '/s', arc_workdir])
subprocess.call(["cmd", "/c", "rmdir", "/q", "/s", arc_workdir])
elif platform.system() == 'Darwin':
elif platform.system() == "Darwin":
status("Expanding flat package...")
subprocess.call(['/usr/sbin/pkgutil', '--expand', pkg_dl_path,
os.path.join(arc_workdir, 'pkg')])
subprocess.call(
[
"/usr/sbin/pkgutil",
"--expand",
pkg_dl_path,
os.path.join(arc_workdir, "pkg"),
]
)
status("Extracting Payload...")
subprocess.call(['/usr/bin/tar', '-xz', '-C', arc_workdir, '-f', os.path.join(arc_workdir, 'pkg', 'Payload')])
output_file = os.path.join(landing_dir, 'WindowsSupport.dmg')
shutil.move(os.path.join(arc_workdir, 'Library/Application Support/BootCamp/WindowsSupport.dmg'),
output_file)
subprocess.call(
[
"/usr/bin/tar",
"-xz",
"-C",
arc_workdir,
"-f",
os.path.join(arc_workdir, "pkg", "Payload"),
]
)
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)
# If we were to also copy out the contents from the .dmg we might do it like this, but if you're doing this
@ -327,36 +425,51 @@ when running the installer out of 'system32'." % output_dir)
# 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 = plistlib.loads(mountxml)
# mntpoint = mountplist['system-entities'][0]['mount-point']
# shutil.copytree(mntpoint, output_dir)
# subprocess.call(['/usr/bin/hdiutil', 'eject', mntpoint])
shutil.rmtree(arc_workdir)
elif platform.system() == 'Linux':
elif platform.system() == "Linux":
status("Extracting for Linux...")
for arc in [pkg_dl_path,
os.path.join(arc_workdir, 'Payload'),
os.path.join(arc_workdir, 'Payload~')]:
for arc in [
pkg_dl_path,
os.path.join(arc_workdir, "Payload"),
os.path.join(arc_workdir, "Payload~"),
]:
if os.path.exists(arc):
status("Extracting {}".format(arc))
subprocess.call(['7z','x',arc, '-o{}'.format(arc_workdir)])
subprocess.call(["7z", "x", arc, "-o{}".format(arc_workdir)])
# BootCamp.pkg (xar) -> Payload (gzip) -> Payload~ (cpio) -> WindowsSupport.dmg
output_file = os.path.join(arc_workdir, 'WindowsSupport.dmg')
shutil.move(os.path.join(arc_workdir, 'Library/Application Support/BootCamp/WindowsSupport.dmg'),
output_file)
subprocess.call(['7z','-o{}'.format(landing_dir),'x','WindowsSupport.dmg',])
output_file = os.path.join(arc_workdir, "WindowsSupport.dmg")
shutil.move(
os.path.join(
arc_workdir,
"Library/Application Support/BootCamp/WindowsSupport.dmg",
),
output_file,
)
subprocess.call(
[
"7z",
"-o{}".format(landing_dir),
"x",
"WindowsSupport.dmg",
]
)
status("Extracted to %s." % output_file)
if os.path.exists('/mnt/c/Windows/explorer.exe'):
subprocess.call(['/mnt/c/Windows/explorer.exe','.'])
if os.path.exists("/mnt/c/Windows/explorer.exe"):
subprocess.call(["/mnt/c/Windows/explorer.exe", "."])
else:
print("Platform not supported! ({})".format(playform.system()))
print("Platform not supported! ({})".format(platform.system()))
pass
status("Done.")
if __name__ == "__main__":
main()