#!/usr/bin/env python3
import os
import sys
import subprocess
import plistlib
import re
import tempfile
import shutil
import optparse
import datetime
import platform
import requests
from urllib import request as urllib_request
from xml.dom import minidom
VERSION = '0.2.6'
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 (22.01)
SEVENZIP_URL = 'https://www.7-zip.org/a/7z2201-x64.msi'
def status(msg):
"""Prints a status message."""
print(f"{msg}\n")
def getCommandOutput(cmd):
"""Executes a command and returns its stdout."""
try:
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = p.communicate()
return out
except OSError as e:
sys.exit(f"Error executing command '{' '.join(cmd)}': {e}")
def getMachineModel():
"""Returns this machine's model identifier."""
if platform.system() == 'Windows':
rawxml = getCommandOutput(['wmic', 'computersystem', 'get', 'model', '/format:RAWXML'])
dom = minidom.parseString(rawxml)
# This is a bit fragile, but it's how the original script did it.
nodes = dom.getElementsByTagName("VALUE")
if nodes and nodes[0].childNodes:
return nodes[0].childNodes[0].data
elif platform.system() == 'Darwin':
plistxml = getCommandOutput(['system_profiler', 'SPHardwareDataType', '-xml'])
plist = plistlib.loads(plistxml)
return plist[0]['_items'][0]['machine_model']
return None
def downloadFile(url, filename, use_requests=False):
"""Downloads a file, showing progress."""
def reporthook(blocknum, blocksize, totalsize):
readsofar = blocknum * blocksize
if totalsize > 0:
percent = readsofar * 1e2 / totalsize
console_out = f"\r{percent:5.1f}% {readsofar:>{len(str(totalsize))}} / {totalsize} bytes"
sys.stderr.write(console_out)
if readsofar >= totalsize:
sys.stderr.write("\n")
else:
sys.stderr.write(f"read {readsofar}\n")
status(f"Downloading {url} to {filename}...")
if use_requests:
try:
resp = requests.get(url, stream=True)
resp.raise_for_status()
with open(filename, 'wb') as fd:
for chunk in resp.iter_content(chunk_size=1024):
fd.write(chunk)
except requests.exceptions.RequestException as e:
sys.exit(f"Error downloading with requests: {e}")
else:
try:
urllib_request.urlretrieve(url, filename, reporthook=reporthook)
except urllib_request.URLError as e:
sys.exit(f"Error downloading with urllib: {e}")
def sevenzipExtract(arcfile, command='e', out_dir=None):
"""Extracts an archive using 7-Zip."""
sevenzip_binary = os.path.join(os.environ.get('ProgramFiles', 'C:\\Program Files'), "7-Zip", "7z.exe")
if not os.path.exists(sevenzip_binary):
sys.exit(f"7-Zip not found at {sevenzip_binary}. Please install it.")
cmd = [sevenzip_binary, command]
if not out_dir:
out_dir = os.path.dirname(arcfile)
cmd.extend(["-o" + out_dir, "-y", arcfile])
status(f"Calling 7-Zip command: {' '.join(cmd)}")
retcode = subprocess.call(cmd)
if retcode:
sys.exit(f"Command failure: {' '.join(cmd)} exited {retcode}.")
def postInstallConfig():
"""Applies post-install configuration on Windows."""
regdata = """Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\\Software\\Apple Inc.\\Apple Keyboard Support]
"FirstTimeRun"=dword:00000000"""
handle, path = tempfile.mkstemp(suffix=".reg")
with os.fdopen(handle, 'w') as fd:
fd.write(regdata)
subprocess.call(['regedit.exe', '/s', path])
os.remove(path)
def findBootcampMSI(search_dir):
"""Returns the path of the 64-bit BootCamp MSI."""
candidates = ['BootCamp64.msi', 'BootCamp.msi']
for root, _, files in os.walk(search_dir):
for msi in candidates:
if msi in files:
return os.path.join(root, msi)
return None
def installBootcamp(msipath):
"""Installs the Boot Camp MSI."""
logpath = os.path.abspath("BootCamp_Install.log")
cmd = ['cmd', '/c', 'msiexec', '/i', msipath, '/qb-', '/norestart', '/log', logpath]
status(f"Executing command: '{' '.join(cmd)}'")
subprocess.call(cmd)
status("Install log output:")
try:
with open(logpath, 'r', encoding='utf-16') as logfd:
logdata = logfd.read()
print(logdata)
except FileNotFoundError:
print(f"Log file not found at {logpath}")
except Exception as e:
print(f"Error reading log file: {e}")
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(s) to use.")
o.add_option('-i', '--install', action="store_true", help="Perform install after download (Windows only).")
o.add_option('-o', '--output-dir', help="Base path to extract files into.")
o.add_option('-k', '--keep-files', action="store_true", help="Keep downloaded/extracted files (used with --install).")
o.add_option('-p', '--product-id', help="Specify an exact product ID to download.")
o.add_option('-V', '--version', action="store_true", help="Output the version of brigadier.")
opts, _ = o.parse_args()
if opts.version:
print(VERSION)
sys.exit(0)
if opts.install:
if platform.system() != 'Windows':
sys.exit("Installing Boot Camp can only be done on Windows!")
if platform.machine() != 'AMD64':
sys.exit("Installing on anything other than 64-bit Windows is not supported!")
output_dir = opts.output_dir or os.getcwd()
if not os.path.isdir(output_dir):
sys.exit(f"Output directory {output_dir} does not exist!")
if not os.access(output_dir, os.W_OK):
sys.exit(f"Output directory {output_dir} is not writable!")
if opts.keep_files and not opts.install:
sys.exit("--keep-files is only useful with --install.")
models = opts.model or [getMachineModel()]
if not models[0]:
sys.exit("Could not determine machine model. Please specify one with -m.")
status(f"Using Mac model(s): {', '.join(models)}")
for model in models:
try:
with urllib_request.urlopen(SUCATALOG_URL) as urlfd:
data = urlfd.read()
except urllib_request.URLError as e:
sys.exit(f"Could not fetch software update catalog: {e}")
p = plistlib.loads(data)
allprods = p.get('Products', {})
bc_prods = [
(prod_id, prod_data) for prod_id, prod_data in allprods.items()
if 'BootCamp' in prod_data.get('ServerMetadataURL', '')
]
pkg_data_list = []
for prod_id, prod_data in bc_prods:
dist_url = prod_data.get('Distributions', {}).get('English')
if not dist_url:
continue
try:
with urllib_request.urlopen(dist_url) as distfd:
dist_data = distfd.read().decode('utf-8', errors='ignore')
if re.search(model, dist_data):
pkg_data_list.append({prod_id: prod_data})
supported_models = re.findall(r"([a-zA-Z]{4,12}[1-9]{1,2},[1-6])", dist_data)
status(f"Model supported in package distribution file: {dist_url}")
status(f"Distribution {prod_id} supports models: {', '.join(supported_models)}")
except urllib_request.URLError:
continue
if not pkg_data_list:
sys.exit(f"Couldn't find a Boot Camp ESD for model {model}.")
pkg_data = None
if len(pkg_data_list) == 1 and not opts.product_id:
pkg_data = pkg_data_list[0]
else:
print("Multiple ESD products available for this model:")
latest_date = datetime.datetime.fromtimestamp(0)
chosen_product_id = None
product_dates = {}
for p_dict in pkg_data_list:
prod_id = list(p_dict.keys())[0]
post_date = p_dict[prod_id].get('PostDate')
product_dates[prod_id] = post_date
print(f"{prod_id}: PostDate {post_date}")
if post_date > latest_date:
latest_date = post_date
chosen_product_id = prod_id
if opts.product_id:
if opts.product_id not in product_dates:
sys.exit(f"Product ID {opts.product_id} not found for model {model}.")
chosen_product_id = opts.product_id
print(f"Selecting manually-chosen product {chosen_product_id}.")
else:
print(f"Selecting {chosen_product_id} as it's the most recent.")
for p_dict in pkg_data_list:
if list(p_dict.keys())[0] == chosen_product_id:
pkg_data = p_dict
break
if not pkg_data:
sys.exit("Failed to select a product package.")
pkg_id = list(pkg_data.keys())[0]
pkg_url = pkg_data[pkg_id]['Packages'][0]['URL']
landing_dir = os.path.join(output_dir, f'BootCamp-{pkg_id}')
if os.path.exists(landing_dir):
status(f"Output path {landing_dir} already exists, removing it...")
shutil.rmtree(landing_dir)
os.makedirs(landing_dir)
status(f"Created directory {landing_dir}")
with tempfile.TemporaryDirectory(prefix="bootcamp-unpack_") as arc_workdir:
pkg_dl_path = os.path.join(arc_workdir, os.path.basename(pkg_url))
downloadFile(pkg_url, pkg_dl_path)
if platform.system() == 'Windows':
status("Windows extraction and installation logic would run here.")
sevenzipExtract(pkg_dl_path, command='x', out_dir=landing_dir)
elif platform.system() == 'Darwin':
status("Expanding flat package...")
subprocess.call(['/usr/sbin/pkgutil', '--expand', pkg_dl_path, os.path.join(arc_workdir, 'pkg')])
payload_path = os.path.join(arc_workdir, 'pkg', 'Payload')
if os.path.exists(payload_path):
status("Extracting Payload...")
subprocess.call(['/usr/bin/tar', '-xz', '-C', arc_workdir, '-f', payload_path])
dmg_source = os.path.join(arc_workdir, 'Library/Application Support/BootCamp/WindowsSupport.dmg')
if os.path.exists(dmg_source):
output_file = os.path.join(landing_dir, 'WindowsSupport.dmg')
shutil.move(dmg_source, output_file)
status(f"Extracted to {output_file}.")
else:
status("WindowsSupport.dmg not found in payload.")
else:
status("Payload not found in package.")
else: # Assuming Linux-like
status("Extraction on Linux requires 'p7zip-full' and 'dmg2img'.")
status("Attempting extraction with 7z...")
subprocess.call(['7z', 'x', pkg_dl_path, f'-o{landing_dir}'])
status("Done.")
if __name__ == "__main__":
main()