brigadier/brigadier.py

373 lines
17 KiB
Python
Executable file

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'
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")
d = downloader.Downloader()
def status(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':
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]\
.getElementsByTagName("PROPERTY")[0].getElementsByTagName("VALUE")[0].childNodes
model = nodes[0].data
elif platform.system() == 'Darwin':
plistxml = getCommandOutput(['system_profiler', 'SPHardwareDataType', '-xml'])
plist = loads_plist(plistxml)
model = plist[0]['_items'][0]['machine_model']
return model
def sevenzipExtract(arcfile, command='e', out_dir=None):
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: {}".format(' '.join(cmd)))
retcode = subprocess.call(cmd)
if 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]
"FirstTimeRun"=dword:00000000"""
handle, path = tempfile.mkstemp()
fd = os.fdopen(handle, 'w')
fd.write(regdata)
fd.close()
subprocess.call(['regedit.exe', '/s', path])
def findBootcampMSI(search_dir):
"""Returns the path of the 64-bit BootCamp MSI"""
# Most ESDs contain 'BootCamp.msi' and 'BootCamp64.msi'
# Dec. 2012 ESD contains only 'BootCamp.msi' which is 64-bit
# The top-level structure of the ESD files depends on whether
# it's an AutoUnattend-based ESD as well, ie:
# /Drivers/Apple/BootCamp64.msi, or
# /BootCamp/Drivers/Apple/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]
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'))
postInstallConfig()
def main():
scriptdir = os.path.abspath(os.path.dirname(sys.argv[0]))
o = optparse.OptionParser()
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",
help="After the installer is downloaded, perform the install automatically. \
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",
help="Keep the files that were downloaded/extracted. Useful only with the \
'--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.")
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:
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 opts.output_dir:
if not os.path.isdir(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 {} 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 {} 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!")
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.")
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: {}.".format(', '.join(models)))
else:
status("Using Mac model: {}.".format(', '.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')
if os.path.isfile(plist_path):
try:
with open(plist_path, "rb") as f:
config_plist = load_plist(f)
except:
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 list(config_plist):
sucatalog_url = config_plist['CatalogURL']
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 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])"
for bc_prod in bc_prods:
if 'English' in list(bc_prod[1]['Distributions']):
disturl = bc_prod[1]['Distributions']['English']
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 {}.".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 {} 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: {}. "
# "Automically selecting the one with the most recent PostDate.."
# .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 = list(p)[0]
postdate = p[product].get('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 [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 {}.".format(chosen_product))
else:
print("Selecting {} as it's the most recently posted.".format(chosen_product))
for p in pkg_data:
if list(p)[0] == chosen_product:
selected_pkg = p
pkg_data = selected_pkg
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 {} 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 {}..".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 {}.".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':
if not check_7z():
print("7-Zip was not found - aborting...")
exit(1)
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~')]:
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)
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])
# clean up the temp dir always
subprocess.call(['cmd', '/c', 'rmdir', '/q', '/s', arc_workdir])
elif platform.system() == 'Darwin':
status("Expanding flat package...")
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)
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 = 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()