Files

1611 lines
59 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Copyright (c) 20242025, LongQT-sea
# mkmaciso the ultimate tool for creating macOS installer ISO and DMG images
# Run with -h or --help to show usage.
# Goals:
# - Use only macOS built-in tools and commands.
# - Download macOS installers exclusively from official Apple sources.
# - Support macOS 10.7 Lion through macOS 26 Tahoe installers.
# - Produce the smallest possible installer images for both formats.
# - The image must be usable on Windows and Linux.
#
# ISO image:
# - A proper UDF DVD/CD format image (mountable in Windows).
# - Intended for virtual machine use; attach as a virtual CD/DVD drive.
# - Compatible with Proxmox VE, QEMU, VirtualBox, and VMware.
#
# DMG image:
# - Raw disk image with GUID Partition Table, this is mandatory so it can be flash to a USB drive using Rufus (Windows).
# - Can also be used with virtual machines, but must be attached as a virtual hard disk.
# - Most VMM require convert the .dmg image to a compatible virtual disk format using qemu-img,
# e.g. convert to .vhd for Hyper-V or .vmdk for VMware. QEMU can use raw disk image without conversion.
#
# For easier use and distribution, the final DMG image will have '.img' appended to its file name.
# Rufus no longer require switching to "All files" to saw the DMG image in Explorer.
# Also fix "qemu-img: Could not locate UDIF trailer in dmg file" error.
#
# While the script is compatible with Apple silicon Macs, its still recommended to run it on an x86_64 Intel Mac for optimal results.
set -e # Exit on error
#set -x # Debug
# Official Apple download URLs for macOS 10.7-10.12 (except 10.9)
# https://support.apple.com/en-hk/102662#browser
LION_URL="https://updates.cdn-apple.com/2021/macos/041-7683-20210614-E610947E-C7CE-46EB-8860-D26D71F0D3EA/InstallMacOSX.dmg"
MOUNTAIN_LION_URL="https://updates.cdn-apple.com/2021/macos/031-0627-20210614-90D11F33-1A65-42DD-BBEA-E1D9F43A6B3F/InstallMacOSX.dmg"
YOSEMITE_URL="http://updates-http.cdn-apple.com/2019/cert/061-41343-20191023-02465f92-3ab5-4c92-bfe2-b725447a070d/InstallMacOSX.dmg"
EL_CAPITAN_URL="http://updates-http.cdn-apple.com/2019/cert/061-41424-20191024-218af9ec-cf50-4516-9011-228c78eda3d2/InstallMacOSX.dmg"
SIERRA_URL="http://updates-http.cdn-apple.com/2019/cert/061-39476-20191023-48f365f4-0015-4c41-9f44-39d3d2aca067/InstallOS.dmg"
# Official Apple download URLs for macOS 10.13-10.15
# https://swscan.apple.com/content/catalogs/others/index-15-14-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog
CATALINA_BASE_URL="https://swcdn.apple.com/content/downloads/26/37/001-68446/r1dbqtmf3mtpikjnd04cq31p4jk91dceh8/"
MOJAVE_BASE_URL="https://swcdn.apple.com/content/downloads/17/32/061-26589-A_8GJTCGY9PC/25fhcu905eta7wau7aoafu8rvdm7k1j4el/"
HIGH_SIERRA_BASE_URL="https://swcdn.apple.com/content/downloads/06/50/041-91758-A_M8T44LH2AW/b5r4og05fhbgatve4agwy4kgkzv07mdid9/"
# Default parameters
VERSION="${1:-}"
IMAGE_FORMAT="${2:-}"
OUTPUT_PATH="${3:-}"
RETRIES_COUNT=10
FINAL_OUTPUT_PATH=""
DISK_ID=""
# Get CPU architecture
CPU_ARCH=$(uname -m)
# Get current macOS kernel version
KERNEL_VERSION=$(uname -r | cut -d'.' -f1)
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color
# Run on macOS only
[ "$(uname -s)" != "Darwin" ] && echo "${RED}Error: macOS only${NC}" && exit 1
# Requires Mavericks (10.9) or newer to run this script
if [ "$KERNEL_VERSION" -lt 13 ]; then
echo "${RED}Unsupported macOS version. Requires Mavericks (10.9) or newer${NC}"
exit 1
fi
# Helper functions
log_info() {
echo -e "${GREEN}$(date +'%H:%M:%S') [INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}$(date +'%H:%M:%S') [WARN]${NC} $1"
}
log_error() {
echo -e "${RED}$(date +'%H:%M:%S') [ERROR]${NC} $1"
}
check_sudo_access() {
if ! sudo -v; then
log_error "This script requires administrator privileges"
exit 1
fi
# Keep sudo alive
while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null &
}
check_disk_space() {
local required_gb
required_gb=40
local threshold_gb
threshold_gb=15
local available
available=$(df -g . | awk 'NR==2 {print $4}')
if [ "$available" -lt "$threshold_gb" ]; then
log_error "CRITICAL: Only ${available} GB available, (${required_gb} GB or more recommended"
log_error "Cannot continue due to low disk space"
exit 1
elif [ "$available" -lt "$required_gb" ]; then
log_error "WARNING: Low disk space - ${available} GB available, ${required_gb} GB or more recommended"
echo ""
echo -ne "${YELLOW}[WARN]${NC} Continue? (Y/n - auto-yes in 10 seconds): "
read -t 10 answer || answer="y"
case "$answer" in
[Nn]|[Nn][Oo])
log_error "Aborted"
exit 1
;;
*)
log_info "Continuing..."
echo ""
;;
esac
fi
}
detach_disk() {
local disk
disk="$1"
[ -z "$disk" ] && return 0
for attempt in $(seq 1 "$RETRIES_COUNT"); do
log_info "Detach attempt $attempt of $RETRIES_COUNT..."
if sync && sleep 5 && hdiutil detach -quiet "$disk" || hdiutil detach -quiet "$disk" -force; then
log_info "Disk detached successfully"
return 0
fi
[ "$attempt" -lt "$RETRIES_COUNT" ] && log_warn "Detach failed, retrying..."
done
log_error "Failed to detach $disk after $RETRIES_COUNT attempts"
return 1
}
cleanup() {
# Only clean up if WORK_DIR is defined and not root/home
if [ -n "$WORK_DIR" ] && [ -d "$WORK_DIR" ] && [ "$WORK_DIR" != "/" ] && [ "$WORK_DIR" != "$HOME" ]; then
# Unmount
for mnt in "$WORK_DIR"/*_mnt; do
[ -d "$mnt" ] && detach_disk "$mnt" 2>/dev/null || true
done
if [ -n "${DISK_ID:-}" ]; then
detach_disk "$DISK_ID"
fi
log_info "Cleaning up temporary files..."
sudo rm -rf "$WORK_DIR"
fi
}
trap cleanup EXIT
get_version_number() {
local codename
codename="$1"
case "$codename" in
"lion") echo "10.7" ;;
"mountainlion") echo "10.8" ;;
"mavericks") echo "10.9" ;;
"yosemite") echo "10.10" ;;
"elcapitan") echo "10.11" ;;
"sierra") echo "10.12" ;;
"highsierra") echo "10.13" ;;
"mojave") echo "10.14" ;;
"catalina") echo "10.15" ;;
"bigsur") echo "11" ;;
"monterey") echo "12" ;;
"ventura") echo "13" ;;
"sonoma") echo "14" ;;
"sequoia") echo "15" ;;
"tahoe") echo "26" ;;
*) echo "" ;;
esac
}
get_codename() {
local version
version="$1"
case "$version" in
"10.7") echo "Lion" ;;
"10.8") echo "Mountain_Lion" ;;
"10.9") echo "Mavericks" ;;
"10.10") echo "Yosemite" ;;
"10.11") echo "El_Capitan" ;;
"10.12") echo "Sierra" ;;
"10.13") echo "High_Sierra" ;;
"10.14") echo "Mojave" ;;
"10.15") echo "Catalina" ;;
"11") echo "Big_Sur" ;;
"12") echo "Monterey" ;;
"13") echo "Ventura" ;;
"14") echo "Sonoma" ;;
"15") echo "Sequoia" ;;
"26") echo "Tahoe" ;;
*) echo "" ;;
esac
}
get_installer_app_name() {
local version_num
version_num="$1"
case "$version_num" in
"10.7")
echo "Install Mac OS X Lion"
;;
"10.8")
echo "Install OS X Mountain Lion"
;;
"10.9")
echo "Install OS X Mavericks"
;;
"10.10")
echo "Install OS X Yosemite"
;;
"10.11")
echo "Install OS X El Capitan"
;;
"10.12")
echo "Install macOS Sierra"
;;
"10.13")
echo "Install macOS High Sierra"
;;
"10.14")
echo "Install macOS Mojave"
;;
"10.15")
echo "Install macOS Catalina"
;;
"11")
echo "Install macOS Big Sur"
;;
"12")
echo "Install macOS Monterey"
;;
"13")
echo "Install macOS Ventura"
;;
"14")
echo "Install macOS Sonoma"
;;
"15")
echo "Install macOS Sequoia"
;;
"26")
echo "Install macOS Tahoe"
;;
*)
echo ""
;;
esac
}
# ==========================
# Interactive Menu Functions
# ==========================
print_header() {
clear
echo -e "${BOLD}${CYAN}"
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
echo "║ mkmaciso the ultimate tool for creating macOS installer ISO and DMG images ║"
echo "╠══════════════════════════════════════════════════════════════════════════════╣"
echo "║ Interactive mode ║"
echo "╚══════════════════════════════════════════════════════════════════════════════╝"
echo -e "${NC}"
}
print_separator() {
echo -e "${CYAN}──────────────────────────────────────────────────────────────────────${NC}"
}
# Array of macOS versions for the menu
declare -a MACOS_VERSIONS=(
"10.7|Lion|2011"
"10.8|Mountain Lion|2012"
"10.9|Mavericks|2013"
"10.10|Yosemite|2014"
"10.11|El Capitan|2015"
"10.12|Sierra|2016"
"10.13|High Sierra|2017"
"10.14|Mojave|2018"
"10.15|Catalina|2019"
"11|Big Sur|2020"
"12|Monterey|2021"
"13|Ventura|2022"
"14|Sonoma|2023"
"15|Sequoia|2024"
"26|Tahoe|2025"
)
show_version_menu() {
print_header
echo -e "${BOLD}Step 1/3: Select macOS Version${NC}"
print_separator
echo ""
local i
i=1
local col
col=0
# Print versions in two columns
echo -e " ${BOLD}# Version Name Year${NC} ${BOLD}# Version Name Year${NC}"
print_separator
local total
total=${#MACOS_VERSIONS[@]}
local half
half=$(( (total + 1) / 2 ))
for ((i=0; i<half; i++)); do
# Left column
IFS='|' read -r ver name year <<< "${MACOS_VERSIONS[$i]}"
printf " ${CYAN}%2d${NC}) %-8s %-16s %s" "$((i+2))" "$ver" "$name" "$year"
# Right column
local right_idx
right_idx=$((i + half))
if [ $right_idx -lt $total ]; then
IFS='|' read -r ver name year <<< "${MACOS_VERSIONS[$right_idx]}"
printf " ${CYAN}%2d${NC}) %-8s %-16s %s" "$((right_idx+2))" "$ver" "$name" "$year"
fi
echo ""
done
echo ""
print_separator
echo ""
echo -e " ${YELLOW}Default: 16 (Tahoe - latest)${NC}"
echo ""
echo -n " Enter your choice [2-$((total+1))] or press Enter for default: "
}
show_format_menu() {
local selected_version
selected_version="$1"
local selected_name
selected_name="$2"
print_header
echo -e "${BOLD}Step 2/3: Select Image Format${NC}"
print_separator
echo ""
echo -e " Selected macOS: $selected_name ($selected_version)"
echo ""
print_separator
echo ""
echo -e " ${CYAN}1${NC}) ${BOLD}ISO${NC} - For virtual machines (Proxmox, QEMU, VMware, VirtualBox)"
echo " Attach to VMs as DVD/CD drive"
echo ""
echo -e " ${CYAN}2${NC}) ${BOLD}DMG${NC} - For USB drives (use Rufus on Windows, dd on Linux)"
echo " Can also be used with VMs as virtual hard disk"
echo ""
print_separator
echo ""
echo -e " ${YELLOW}Default: [1] ISO${NC}"
echo ""
echo -n " Enter your choice [1-2] or press Enter for default: "
}
show_output_menu() {
local selected_version
selected_version="$1"
local selected_name
selected_name="$2"
local selected_format
selected_format="$3"
print_header
echo -e "${BOLD}Step 3/3: Select Output Location${NC}"
print_separator
echo ""
echo -e " Selected macOS: $selected_name ($selected_version)"
echo -e " Selected Format: $(echo "$selected_format" | tr '[:lower:]' '[:upper:]')"
echo ""
print_separator
echo ""
echo -e " ${CYAN}1${NC}) ${BOLD}Current Directory${NC}"
echo " $(pwd)"
echo ""
echo -e " ${CYAN}2${NC}) ${BOLD}Desktop${NC}"
echo " $HOME/Desktop"
echo ""
echo -e " ${CYAN}3${NC}) ${BOLD}Downloads${NC}"
echo " $HOME/Downloads"
echo ""
echo -e " ${CYAN}4${NC}) ${BOLD}Home Directory${NC}"
echo " $HOME"
echo ""
echo -e " ${CYAN}5${NC}) ${BOLD}Custom Path${NC}"
echo " Enter your own path"
echo ""
print_separator
echo ""
echo -e " ${YELLOW}Default: [1] Current Directory${NC}"
echo ""
echo -n " Enter your choice [1-5] or press Enter for default: "
}
show_confirmation() {
local selected_version
selected_version="$1"
local selected_name
selected_name="$2"
local selected_format
selected_format="$3"
local output_file
output_file="$4"
print_header
echo -e "${BOLD}Confirmation${NC}"
print_separator
echo ""
echo -e " ${BOLD}macOS Version:${NC} $selected_name ($selected_version)"
echo -e " ${BOLD}Image Format:${NC} $(echo "$selected_format" | tr '[:lower:]' '[:upper:]')"
echo -e " ${BOLD}Output File:${NC} $output_file"
echo ""
print_separator
echo ""
echo -n " Proceed with these settings? [Y/n]: "
}
run_interactive_menu() {
local selected_version
selected_version=""
local selected_name
selected_name=""
local selected_format
selected_format=""
local output_dir
output_dir=""
local output_file
output_file=""
# Step 1: Select macOS version
while true; do
show_version_menu
read -r choice
# Default to Tahoe
if [ -z "$choice" ]; then
choice=16
fi
# Validate input
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 2 ] && [ "$choice" -le $((${#MACOS_VERSIONS[@]} + 1)) ]; then
local idx
idx=$((choice - 2))
IFS='|' read -r selected_version selected_name _ <<< "${MACOS_VERSIONS[$idx]}"
break
else
echo -e "\n ${RED}Invalid choice. Please enter a number between 2 and $((${#MACOS_VERSIONS[@]} + 1))${NC}"
sleep 2
fi
done
# Step 2: Select format
while true; do
show_format_menu "$selected_version" "$selected_name"
read -r choice
# Default to ISO
if [ -z "$choice" ]; then
choice=1
fi
case "$choice" in
1)
selected_format="iso"
break
;;
2)
selected_format="dmg"
break
;;
*)
echo -e "\n ${RED}Invalid choice. Please enter 1 or 2${NC}"
sleep 2
;;
esac
done
# Step 3: Select output location
while true; do
show_output_menu "$selected_version" "$selected_name" "$selected_format"
read -r choice
# Default to current directory
if [ -z "$choice" ]; then
choice=1
fi
case "$choice" in
1)
output_dir="$(pwd)"
break
;;
2)
output_dir="$HOME/Desktop"
break
;;
3)
output_dir="$HOME/Downloads"
break
;;
4)
output_dir="$HOME"
break
;;
5)
echo ""
echo -n " Enter custom path: "
read -r custom_path
# Expand ~ if present
custom_path="${custom_path/#\~/$HOME}"
if [ -d "$custom_path" ]; then
output_dir="$custom_path"
break
else
echo -e "\n ${RED}Directory does not exist: $custom_path${NC}"
echo -n " Create it? [y/N]: "
read -r create_dir
if [[ "$create_dir" =~ ^[Yy] ]]; then
mkdir -p "$custom_path"
output_dir="$custom_path"
break
fi
fi
;;
*)
echo -e "\n ${RED}Invalid choice. Please enter 1-5${NC}"
sleep 2
;;
esac
done
# Format the version name for filename (replace spaces with underscores)
local filename_name
filename_name="${selected_name// /_}"
output_file="${output_dir}/macOS_${filename_name}.${selected_format}"
# Confirmation
show_confirmation "$selected_version" "$selected_name" "$selected_format" "$output_file"
read -r confirm
if [[ "$confirm" =~ ^[Nn] ]]; then
echo ""
echo -e " ${YELLOW}Aborted by user${NC}"
echo ""
exit 0
fi
# Set global variables for main script
VERSION="$selected_version"
IMAGE_FORMAT="$selected_format"
OUTPUT_PATH="$output_file"
clear
}
# =================================
# End of Interactive Menu Functions
# =================================
download_legacy_macos() {
local version_num
version_num="$1"
local download_url
download_url=""
case "$version_num" in
"10.7")
download_url="$LION_URL"
;;
"10.8")
download_url="$MOUNTAIN_LION_URL"
;;
"10.9")
download_mavericks
return $?
;;
"10.10")
download_url="$YOSEMITE_URL"
;;
"10.11")
download_url="$EL_CAPITAN_URL"
;;
"10.12")
download_url="$SIERRA_URL"
;;
*)
log_error "Unsupported legacy macOS version: $version_num"
return 1
;;
esac
log_info "Downloading macOS $(get_codename $version_num)..."
# If download does not finish within 30 minutes (--max-time 1800), redownload to establish a new connection.
curl --retry 5 --max-time 1800 --connect-timeout 10 --progress-bar -L "$download_url" -o "$WORK_DIR/InstallMacOSX.dmg"
}
# macOS 10.9 Mavericks requires special handling:
# 1. Must use Apple's recovery server protocol to download
# 2. Requires specific hardware identifiers for authentication
# Reference: https://mavericksforever.com/get.sh
download_mavericks() {
local board_serial_number
board_serial_number="C0243070168G3M91F"
local board_id
board_id="Mac-3CBD00234E554E41"
local rom
rom="003EE1E6AC14"
hex_to_bin() { printf "%s" "$1" | xxd -r -p; }
# Get server ID
local server_id
server_id=$(curl -fs -c - http://osrecovery.apple.com/ | tail -1 | awk '{print $NF}')
# Generate client ID
local client_id
client_id=$(dd if=/dev/urandom bs=8 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n' | tr '[:lower:]' '[:upper:]')
# Create key_info file
{
hex_to_bin "$client_id"
hex_to_bin "$(echo $server_id | awk -F'~' '{print $2}')"
hex_to_bin "$rom"
printf "%s" "${board_serial_number}${board_id}" | iconv -t utf-8 | openssl dgst -sha256 -binary
printf '\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC'
} > "$WORK_DIR/key_info"
# Generate key
local key
key=$(openssl dgst -sha256 -binary < "$WORK_DIR/key_info" | od -An -tx1 | tr -d ' \n' | tr '[:lower:]' '[:upper:]')
rm "$WORK_DIR/key_info"
# Get installation payload
local installation_payload
installation_payload=$(curl -fs 'http://osrecovery.apple.com/InstallationPayload/OSInstaller' -X POST \
-H 'Content-Type: text/plain' \
--cookie "session=$server_id" \
-d "cid=$client_id
sn=$board_serial_number
bid=$board_id
k=$key")
# Extract asset URL and token
local mavericks_url
mavericks_url=$(echo "$installation_payload" | grep AU | awk -F': ' '{print $2}')
local token
token=$(echo "$installation_payload" | grep AT | awk -F': ' '{print $2}')
log_info "Mavericks URL detected: $mavericks_url"
echo ""
log_info "Downloading macOS Mavericks using special method..."
# Download Mavericks InstallESD.dmg
# If download does not finish within 30 minutes (--max-time 1800), redownload to establish a new connection.
curl --retry 5 --max-time 1800 --connect-timeout 10 --progress-bar -L "$mavericks_url" -H "Cookie: AssetToken=$token" -o "$WORK_DIR/InstallESD.dmg"
}
install_legacy_app() {
local version_num
version_num="$1"
local dmg_file
dmg_file="$2"
log_info "Installing macOS $(get_codename $version_num) installer to /Applications..."
if [[ "$version_num" =~ ^(10\.7|10\.8|10\.10|10\.11|10\.12)$ ]]; then
# Mount the downloaded DMG image
hdiutil attach -nobrowse -quiet -mountpoint "$WORK_DIR/downloaded_dmg_mnt" "$dmg_file"
# Manual installation process for Apple silicon.
# The 'Install OSX/macOS.app' produced by manual install is not suitable
# for creating a bootable USB using 'createinstallmedia'.
if [ "$CPU_ARCH" == "arm64" ]; then
log_info "Apple silicon detected, using manual installation..."
# Extract the package
pkgutil --expand "$WORK_DIR/downloaded_dmg_mnt"/*.pkg "$WORK_DIR/pkg_extracted"
# Find the inner package directory (InstallMacOSX.pkg)
local inner_pkg
inner_pkg=$(ls -d "$WORK_DIR/pkg_extracted"/*.pkg 2>/dev/null | head -1)
# Extract the app from Payload to /Applications
cd /Applications
sudo cpio -idm < "$inner_pkg/Payload"
cd - > /dev/null
# Copy InstallESD.dmg
local app_path
app_path="/Applications/$(get_installer_app_name $version_num).app"
sudo ditto "$inner_pkg/InstallESD.dmg" "$app_path/Contents/SharedSupport/InstallESD.dmg"
sudo chown root:wheel "$app_path/Contents/SharedSupport/InstallESD.dmg"
log_info "Installed $(get_installer_app_name $version_num) to /Applications"
log_warn "Note: This $(get_installer_app_name $version_num) is not suitable for creating a bootable USB using 'createinstallmedia'"
else
# Standard installation for Intel Macs
sudo installer -pkg "$WORK_DIR/downloaded_dmg_mnt"/*.pkg -target / && \
log_info "Installed $(get_installer_app_name $version_num) to /Applications"
fi
sync && detach_disk "$WORK_DIR/downloaded_dmg_mnt"
elif [ "$version_num" == "10.9" ]; then
# Check if InstallESD.dmg exists
if [ -f "$WORK_DIR/InstallESD.dmg" ]; then
# Mount InstallESD.dmg and BaseSystem.dmg inside it
hdiutil attach -nobrowse -quiet -mountpoint "$WORK_DIR/InstallESD_mnt" "$WORK_DIR/InstallESD.dmg"
hdiutil attach -nobrowse -quiet -mountpoint "$WORK_DIR/BaseSystem_mnt" "$WORK_DIR/InstallESD_mnt/BaseSystem.dmg"
# Copy required files
sudo cp -a "$WORK_DIR/BaseSystem_mnt/Install OS X Mavericks.app" "/Applications/"
sudo mkdir "/Applications/Install OS X Mavericks.app/Contents/SharedSupport"
sudo cp -a "$WORK_DIR/InstallESD.dmg" "/Applications/Install OS X Mavericks.app/Contents/SharedSupport/InstallESD.dmg"
sudo cp -a "$WORK_DIR/InstallESD_mnt/Packages/OSInstall.mpkg" "/Applications/Install OS X Mavericks.app/Contents/SharedSupport/"
sudo chown -R root:wheel "/Applications/Install OS X Mavericks.app/Contents/SharedSupport/"
log_info "Installed $(get_installer_app_name $version_num) to /Applications"
# Unmount
sync && detach_disk "$WORK_DIR/BaseSystem_mnt"
sync && detach_disk "$WORK_DIR/InstallESD_mnt"
fi
fi
}
# Reference: https://www.insanelymac.com/forum/topic/338810-create-legit-copy-of-macos-from-apple-catalog/
direct_download_10_13_10_15() {
local version_num
version_num="$1"
local base_url
base_url=""
# Determine base URL and version name
case "$version_num" in
"10.13")
base_url="$HIGH_SIERRA_BASE_URL"
;;
"10.14")
base_url="$MOJAVE_BASE_URL"
;;
"10.15")
base_url="$CATALINA_BASE_URL"
;;
*)
return 1
;;
esac
log_info "Downloading macOS $(get_codename $version_num) installer using direct Apple URLs..."
echo ""
# Required files for 10.13-10.15
local files
files=(
"BaseSystem.dmg"
"BaseSystem.chunklist"
"InstallInfo.plist"
"InstallESDDmg.pkg"
"AppleDiagnostics.dmg"
"AppleDiagnostics.chunklist"
)
# Download all required files
for filename in "${files[@]}"; do
log_info "Downloading ${filename}..."
curl --retry 5 --max-time 1800 --connect-timeout 10 --progress-bar \
-L "${base_url}${filename}" -o "$WORK_DIR/${filename}"
if [ $? -ne 0 ]; then
log_error "Failed to download ${filename}"
return 1
fi
done
echo ""
log_info "All files downloaded. Building macOS $(get_codename $version_num) installer app..."
# Mount BaseSystem.dmg to extract the app
hdiutil attach -nobrowse -quiet -mountpoint "$WORK_DIR/BaseSystem_mnt" "$WORK_DIR/BaseSystem.dmg"
# Copy installer app to /Applications
local installer_app
installer_app="$WORK_DIR/BaseSystem_mnt/$(get_installer_app_name $version_num).app"
sudo cp -Rp "$installer_app" "/Applications/"
# Create SharedSupport directory
local shared_support
shared_support="/Applications/$(get_installer_app_name $version_num).app/Contents/SharedSupport"
sudo mkdir -p "$shared_support"
# Rename InstallESDDmg.pkg to InstallESD.dmg
mv "$WORK_DIR/InstallESDDmg.pkg" "$WORK_DIR/InstallESD.dmg"
# Fix InstallInfo.plist
sed -e "s/InstallESDDmg\.pkg/InstallESD.dmg/" \
-e "s/pkg\.InstallESDDmg/dmg.InstallESD/" \
-e "/InstallESD\.dmg/{n;N;N;N;d;}" \
"$WORK_DIR/InstallInfo.plist" > "$WORK_DIR/InstallInfo_fixed.plist"
# Copy files to SharedSupport
sudo cp -Rp "$WORK_DIR/BaseSystem.dmg" "$shared_support/"
sudo cp -Rp "$WORK_DIR/BaseSystem.chunklist" "$shared_support/"
sudo cp -Rp "$WORK_DIR/InstallInfo_fixed.plist" "$shared_support/InstallInfo.plist"
sudo cp -Rp "$WORK_DIR/InstallESD.dmg" "$shared_support/"
sudo cp -Rp "$WORK_DIR/AppleDiagnostics.dmg" "$shared_support/"
sudo cp -Rp "$WORK_DIR/AppleDiagnostics.chunklist" "$shared_support/"
# Set proper ownership
sudo chown -R root:wheel "$shared_support"
# Resign createinstallmedia and remove quarantine tag for Apple Silicon
if [ "$CPU_ARCH" == "arm64" ] && [[ "$version_num" =~ ^10\.1[3-5]$ ]]; then
sudo codesign -s - -f "/Applications/$(get_installer_app_name $version_num).app/Contents/Resources/createinstallmedia"
sudo xattr -r -d com.apple.quarantine "/Applications/$(get_installer_app_name $version_num).app"
fi
# Detach BaseSystem.dmg
sync && sleep 10 && detach_disk "$WORK_DIR/BaseSystem_mnt"
log_info "Successfully installed $(get_installer_app_name $version_num) to /Applications"
return 0
}
download_modern_macos() {
local version_num
version_num="$1"
version_name=$(get_codename $version_num)
version_name="${version_name/_/ }"
# Use direct download when softwareupdate is not available
if [[ "$version_num" =~ ^10\.1[345]$ ]]; then
# For Apple silicon: always use direct download for 10.13-10.15
if [ "$CPU_ARCH" == "arm64" ]; then
log_info "Apple silicon detected"
echo ""
# Show warning on Apple silicon
log_warn "╔═════════════════════════════════════════════════════════════════╗"
log_warn "║ Apple silicon Limitation ║"
log_warn "╠═════════════════════════════════════════════════════════════════╣"
log_warn "║ On Apple silicon, softwareupdate cannot download macOS versions ║"
log_warn "║ older than the one that originally shipped with your device. ║"
log_warn "╚═════════════════════════════════════════════════════════════════╝"
echo ""
direct_download_10_13_10_15 "$version_num"
return $?
fi
# For Intel: use direct download if running kernel < 20 (pre-Big Sur)
if [ "$CPU_ARCH" = "x86_64" ] && [ "$KERNEL_VERSION" -lt 20 ]; then
log_info "Intel CPU detected"
log_info "This Mac is running a version older than Big Sur, will attempt direct download for macOS $version_name"
direct_download_10_13_10_15 "$version_num"
return $?
fi
fi
# Use softwareupdate for macOS Big Sur and newer
# Check CPU architecture compatibility for softwareupdate
if [ "$CPU_ARCH" = "x86_64" ] && [ "$KERNEL_VERSION" -lt 20 ]; then
log_info "Intel CPU detected"
# Intel Macs require macOS 11 or newer for softwareupdate
log_error "Intel Macs must be running macOS 11 (Big Sur) or newer to download macOS using softwareupdate"
log_error "Please upgrade your macOS to version 11 or newer"
return 1
elif [ "$CPU_ARCH" == "arm64" ]; then
log_info "Apple silicon detected"
# Show warning on Apple silicon
log_warn "╔═════════════════════════════════════════════════════════════════╗"
log_warn "║ Apple silicon Limitation ║"
log_warn "╠═════════════════════════════════════════════════════════════════╣"
log_warn "║ On Apple silicon, softwareupdate cannot download macOS versions ║"
log_warn "║ older than the one that originally shipped with your device. ║"
log_warn "╚═════════════════════════════════════════════════════════════════╝"
fi
log_info "Fetching available macOS installers..."
softwareupdate --list-full-installers 2>/dev/null | grep "* Title:" | sed 's/^[[:space:]]*//' > "$WORK_DIR/installers.txt"
# Get latest installer version
local selected_version_number
selected_version_number=$(grep -i "$version_name" "$WORK_DIR/installers.txt" | grep -iv "beta" | head -1 | sed -n 's/.*Version: \([^,]*\).*/\1/p')
if [ -z "$selected_version_number" ]; then
log_error "macOS $version_name ($version_num) is not available for download on this system"
log_info "Available installers:"
cat "$WORK_DIR/installers.txt"
return 1
fi
log_info "Found latest $version_name version: $selected_version_number"
log_info "Downloading and installing macOS $version_name installer (this may take a while)..."
# Download with retries (max 4 times)
local download_retries
download_retries=4
for attempt in $(seq 1 $download_retries); do
log_info "Download attempt $attempt of $download_retries..."
if softwareupdate --fetch-full-installer --full-installer-version "$selected_version_number"; then
log_info "Download successful!"
sleep 5 && return 0
fi
if [ $attempt -eq $download_retries ]; then
log_error "Download failed after $download_retries attempts"
return 1
fi
log_warn "Retrying in 5 seconds..."
sleep 5
done
}
create_iso_dmg_10_7_10_8() {
local installer_path
installer_path="$1"
local output_file
output_file="$2"
local volume_name
volume_name="$3"
local version_num
version_num="$4"
local image_format
image_format=$(echo "$IMAGE_FORMAT" | tr '[:lower:]' '[:upper:]')
log_info "Creating macOS $(get_codename $version_num) $image_format image..."
echo ""
# Mount InstallESD.dmg
hdiutil attach -nobrowse -quiet -mountpoint "$WORK_DIR/InstallESD_mnt" "$installer_path/Contents/SharedSupport/InstallESD.dmg"
# Extract the exact version number and append it to the file name
local exact_version_number
exact_version_number=$(defaults read "$WORK_DIR/InstallESD_mnt/System/Library/CoreServices/SystemVersion" ProductVersion)
FINAL_OUTPUT_PATH="${output_file%.${IMAGE_FORMAT}}_$exact_version_number.${IMAGE_FORMAT}"
# Create final image
if [ "$IMAGE_FORMAT" == "dmg" ]; then
# Create DMG with GUID Partition Table from mounted InstallESD.dmg
sudo hdiutil create -quiet -layout GPTSPUD -format UDRW -ov -volname "$volume_name" -srcdir "$WORK_DIR/InstallESD_mnt" "$FINAL_OUTPUT_PATH"
# Append .img extension
mv -f "$FINAL_OUTPUT_PATH" "${FINAL_OUTPUT_PATH}.img"
FINAL_OUTPUT_PATH="${FINAL_OUTPUT_PATH}.img"
# Unmount
sync && detach_disk "$WORK_DIR/InstallESD_mnt"
else
# Unmount
sync && detach_disk "$WORK_DIR/InstallESD_mnt"
log_info "Converting to ISO format..."
sudo hdiutil makehybrid -quiet -ov -hfs -udf -default-volume-name "$volume_name" "$installer_path/Contents/SharedSupport/InstallESD.dmg" -o "$FINAL_OUTPUT_PATH"
fi
}
create_iso_10_9_10_11() {
local installer_path
installer_path="$1"
local output_file
output_file="$2"
local volume_name
volume_name="$3"
local version_num
version_num="$4"
log_info "Creating macOS $(get_codename $version_num) ISO image..."
# Mount InstallESD.dmg
hdiutil attach -nobrowse -quiet -mountpoint "$WORK_DIR/InstallESD_mnt" "$installer_path/Contents/SharedSupport/InstallESD.dmg"
# Convert BaseSystem.dmg
hdiutil convert -quiet "$WORK_DIR/InstallESD_mnt/BaseSystem.dmg" -format UDRW -o "$WORK_DIR/BaseSystem_converted.dmg"
# Increase size
sudo hdiutil resize -size 7.1g "$WORK_DIR/BaseSystem_converted.dmg"
# Mount the converted DMG
DISK_ID=$(hdiutil attach -nobrowse -mountpoint "$WORK_DIR/BaseSystem_mnt" "$WORK_DIR/BaseSystem_converted.dmg" | grep -o '/dev/disk[0-9]*' | head -1)
# Remove Packages symlink and copy actual Packages
rm "$WORK_DIR/BaseSystem_mnt/System/Installation/Packages"
cp -R "$WORK_DIR/InstallESD_mnt/Packages" "$WORK_DIR/BaseSystem_mnt/System/Installation/"
# Copy required files
cp "$WORK_DIR/InstallESD_mnt/BaseSystem.dmg" "$WORK_DIR/BaseSystem_mnt/"
cp "$WORK_DIR/InstallESD_mnt/BaseSystem.chunklist" "$WORK_DIR/BaseSystem_mnt/"
# Note: For Mavericks UDF format, boot.efi attempts to load /mach_kernel, but none exists.
# Although we can extract it from Packages/BaseSystemBinaries.pkg, there's a simpler solution:
if [ "$version_num" == "10.9" ]; then
cp "$WORK_DIR/BaseSystem_mnt/System/Library/Caches/com.apple.kext.caches/Startup/kernelcache" "$WORK_DIR/BaseSystem_mnt/mach_kernel"
fi
# Extract the exact version number and append it to the file name
local exact_version_number
exact_version_number=$(defaults read "$WORK_DIR/BaseSystem_mnt/System/Library/CoreServices/SystemVersion" ProductVersion)
FINAL_OUTPUT_PATH="${output_file%.iso}_$exact_version_number.iso"
# Unmount
detach_disk "$WORK_DIR/InstallESD_mnt"
detach_disk "$DISK_ID" && DISK_ID=""
# Create ISO
log_info "Converting to ISO format..."
sudo hdiutil makehybrid -quiet -ov -hfs -udf -default-volume-name "$volume_name" \
"$WORK_DIR/BaseSystem_converted.dmg" -o "$FINAL_OUTPUT_PATH"
}
create_dmg_10_9_to_10_12_alt_method() {
local installer_path
installer_path="$1"
local output_file
output_file="$2"
local volume_name
volume_name="$3"
local version_num
version_num="$4"
log_info "Creating macOS $(get_codename $version_num) DMG image..."
echo ""
# Mount InstallESD.dmg
hdiutil attach -nobrowse -quiet -mountpoint "$WORK_DIR/InstallESD_mnt" "$installer_path/Contents/SharedSupport/InstallESD.dmg"
if [[ "$version_num" =~ ^(10\.9|10\.10)$ ]]; then
# Attach BaseSystem.dmg
BaseSystem_DISK_ID=$(hdiutil attach "$WORK_DIR/InstallESD_mnt/BaseSystem.dmg" -nomount | grep -o '/dev/disk[0-9]*' | head -1)
# Create a DMG with GUID Partition Table and attach it
hdiutil create -quiet -ov -fs hfs+ -size 1400m "$WORK_DIR/installer_gpt.dmg"
DISK_ID=$(hdiutil attach "$WORK_DIR/installer_gpt.dmg" -nomount | grep -o '/dev/disk[0-9]*' | head -1)
# Write the bootable recovery partition to the first partition on the DMG image
sudo dd if="${BaseSystem_DISK_ID}s2" of="${DISK_ID}s1" bs=1m && detach_disk "$BaseSystem_DISK_ID"
# Unmount
detach_disk "$DISK_ID" && DISK_ID=""
# Increase size
sudo hdiutil resize -size 7g "$WORK_DIR/installer_gpt.dmg"
elif [[ "$version_num" =~ ^(10\.11|10\.12)$ ]]; then
local dmg_size
case "$version_num" in
"10.11")
dmg_size="7.1"
;;
*)
dmg_size="5.9"
;;
esac
# El Capitan and Sierra BaseSystem.dmg already use a GUID Partition Table
# Convert BaseSystem.dmg
hdiutil convert -quiet "$WORK_DIR/InstallESD_mnt/BaseSystem.dmg" -format UDRW -o "$WORK_DIR/installer_gpt.dmg"
# Increase size
sudo hdiutil resize -size ${dmg_size}g "$WORK_DIR/installer_gpt.dmg"
fi
# Mount DMG image and change volume name
DISK_ID=$(hdiutil attach -nobrowse -mountpoint "$WORK_DIR/installer_mnt" "$WORK_DIR/installer_gpt.dmg" | grep -o '/dev/disk[0-9]*' | head -1)
diskutil rename "${DISK_ID}s1" "$volume_name"
# Remove Packages symlink and copy actual Packages
rm "$WORK_DIR/installer_mnt/System/Installation/Packages"
ditto "$WORK_DIR/InstallESD_mnt/Packages" "$WORK_DIR/installer_mnt/System/Installation/Packages"
# Copy required files
cp -R "$WORK_DIR/InstallESD_mnt/BaseSystem.dmg" "$WORK_DIR/installer_mnt/"
cp -R "$WORK_DIR/InstallESD_mnt/BaseSystem.chunklist" "$WORK_DIR/installer_mnt/"
# Extract the exact version number and append it to the file name
local exact_version_number
exact_version_number=$(defaults read "$WORK_DIR/installer_mnt/System/Library/CoreServices/SystemVersion" ProductVersion)
FINAL_OUTPUT_PATH="${output_file%.dmg}_$exact_version_number.dmg.img"
sync && sleep 5
# Unmount
detach_disk "$WORK_DIR/InstallESD_mnt"
detach_disk "$DISK_ID" && DISK_ID=""
# Optimize DMG file size
log_info "Optimizing DMG file size..."
sudo hdiutil resize -size min "$WORK_DIR/installer_gpt.dmg"
# Move to output
log_info "Move to $FINAL_OUTPUT_PATH"
mv -f "$WORK_DIR/installer_gpt.dmg" "$FINAL_OUTPUT_PATH"
}
create_iso_dmg_10_15_alt_method() {
local installer_path
installer_path="$1"
local output_file
output_file="$2"
local volume_name
volume_name="$3"
local version_num
version_num="$4"
local image_format
image_format=$(echo "$IMAGE_FORMAT" | tr '[:lower:]' '[:upper:]')
log_info "Apple Silicon detected. Using alternative method for Catalina instead of 'createinstallmedia'"
echo ""
log_info "Creating macOS $(get_codename $version_num) $image_format image..."
echo ""
# Mount InstallESD.dmg
hdiutil attach -nobrowse -quiet -mountpoint "$WORK_DIR/InstallESD_mnt" "$installer_path/Contents/SharedSupport/InstallESD.dmg"
# Convert BaseSystem.dmg
local basesystem_path
basesystem_path="$installer_path/Contents/SharedSupport/BaseSystem.dmg"
hdiutil convert -quiet "$basesystem_path" -format UDRW -o "$WORK_DIR/BaseSystem_converted.dmg"
# Increase size
sudo hdiutil resize -size 9g "$WORK_DIR/BaseSystem_converted.dmg"
# Mount the converted DMG
DISK_ID=$(hdiutil attach -nobrowse -mountpoint "$WORK_DIR/BaseSystem_mnt" "$WORK_DIR/BaseSystem_converted.dmg" | grep -o '/dev/disk[0-9]*' | head -1)
diskutil rename "${DISK_ID}s1" "$volume_name"
# Remove the stub installer app
rm -rf "$WORK_DIR/BaseSystem_mnt/$(get_installer_app_name $version_num).app"
# Copy actual installer app from /Applications
cp -R "$installer_path" "$WORK_DIR/BaseSystem_mnt/"
# Extract the exact version number and append it to the file name
local exact_version_number
exact_version_number=$(defaults read "$WORK_DIR/BaseSystem_mnt/System/Library/CoreServices/SystemVersion" ProductVersion)
if [ "$IMAGE_FORMAT" == "dmg" ]; then
FINAL_OUTPUT_PATH="${output_file%.dmg}_$exact_version_number.dmg.img"
else
FINAL_OUTPUT_PATH="${output_file%.iso}_$exact_version_number.iso"
fi
# Unmount
detach_disk "$WORK_DIR/InstallESD_mnt"
detach_disk "$DISK_ID" && DISK_ID=""
# Create final image
if [ "$IMAGE_FORMAT" == "dmg" ]; then
# Optimize DMG file size
log_info "Optimizing DMG file size..."
sudo hdiutil resize -size min "$WORK_DIR/BaseSystem_converted.dmg"
# Move to output
log_info "Move to $FINAL_OUTPUT_PATH"
mv -f "$WORK_DIR/BaseSystem_converted.dmg" "$FINAL_OUTPUT_PATH"
else
log_info "Converting to ISO format..."
sudo hdiutil makehybrid -quiet -ov -hfs -udf -default-volume-name "$volume_name" "$WORK_DIR/BaseSystem_converted.dmg" -o "$FINAL_OUTPUT_PATH"
fi
}
create_iso_10_12_and_later() {
local installer_path
installer_path="$1"
local output_file
output_file="$2"
local version_num
version_num="$3"
# macOS Sierra and Catalina 'createinstallmedia' fails when running on Apple silicon
# Redirecting to alternative method
if [[ "$CPU_ARCH" == "arm64" ]]; then
if [[ "$version_num" == "10.12" ]]; then
create_iso_10_9_10_11 \
"$installer_path" \
"$output_file" \
"$(get_installer_app_name "$version_num")" \
"$version_num"
return $?
elif [[ "$version_num" == "10.15" ]]; then
create_iso_dmg_10_15_alt_method \
"$installer_path" \
"$output_file" \
"$(get_installer_app_name "$version_num")" \
"$version_num"
return $?
fi
fi
log_info "Creating macOS $(get_codename $version_num) ISO image..."
echo ""
# Create sparse image
hdiutil create -quiet -size 20g -volname "$(get_codename $version_num)_iso" -fs HFS+ -type SPARSE -attach "$WORK_DIR/installer.sparseimage"
DISK_ID=$(diskutil list | grep $(get_codename $version_num)_iso | head -1 | awk '{print $NF}' | sed 's/s[0-9]*$//')
# Prepare createinstallmedia command
local cmd
cmd="sudo '$installer_path/Contents/Resources/createinstallmedia' --volume /Volumes/$(get_codename $version_num)_iso --nointeraction"
# For macOS 10.12, need to patch Info.plist
# For macOS 10.10-10.12, need to add --applicationpath
if [ "$version_num" == "10.12" ]; then
log_info "Patching Info.plist for Sierra compatibility..."
sudo /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString '12.6.03'" "$installer_path/Contents/Info.plist"
cmd="$cmd --applicationpath \"$installer_path\""
fi
# Create install media
log_info "Running createinstallmedia..."
bash -c "$cmd"
sync && sleep 5
# Extract the exact version number and append it to the file name
local exact_version_number
exact_version_number=$(defaults read "/Volumes/$(get_installer_app_name "$version_num")/System/Library/CoreServices/SystemVersion" ProductVersion)
FINAL_OUTPUT_PATH="${output_file%.iso}_$exact_version_number.iso"
# Restore Info.plist
if [ "$version_num" == "10.12" ]; then
sudo /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString '12.6.06'" "/Applications/Install macOS Sierra.app/Contents/Info.plist"
sudo /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString '12.6.06'" "/Volumes/Install macOS Sierra/Install macOS Sierra.app/Contents/Info.plist"
fi
# Unmount
detach_disk "$DISK_ID" && DISK_ID=""
# Create ISO from sparse image
log_info "Converting to ISO format..."
sudo hdiutil makehybrid -quiet -ov -hfs -udf "$WORK_DIR/installer.sparseimage" -o "$FINAL_OUTPUT_PATH"
}
create_dmg_10_10_and_later() {
local installer_path
installer_path="$1"
local output_file
output_file="$2"
local version_num
version_num="$3"
# macOS Catalina, Sierra, El Capitan, and Yosemite 'createinstallmedia' fails when running on Apple silicon
# Redirecting to alternative method
if [[ "$CPU_ARCH" == "arm64" ]]; then
if [[ "$version_num" =~ ^10\.1[012]$ ]]; then
create_dmg_10_9_to_10_12_alt_method \
"$installer_path" \
"$output_file" \
"$(get_installer_app_name "$version_num")" \
"$version_num"
return $?
elif [[ "$version_num" = "10.15" ]]; then
create_iso_dmg_10_15_alt_method \
"$installer_path" \
"$output_file" \
"$(get_installer_app_name "$version_num")" \
"$version_num"
return $?
fi
fi
log_info "Creating macOS $(get_codename $version_num) DMG image..."
echo ""
# Determine DMG size based on macOS version
local dmg_size
case "$version_num" in
10.10|10.11|10.12|10.13|10.14)
dmg_size="7g"
;;
10.15)
dmg_size="9g"
;;
11|12|13)
dmg_size="15g"
;;
14|15)
dmg_size="18g"
;;
*)
dmg_size="20g"
;;
esac
# Create DMG disk image
hdiutil create -quiet -size $dmg_size -volname "$(get_codename $version_num)_dmg" -fs HFS+ -type UDIF -attach "$WORK_DIR/installer.dmg"
DISK_ID=$(diskutil list | grep $(get_codename $version_num)_dmg | head -1 | awk '{print $NF}' | sed 's/s[0-9]*$//')
# Prepare createinstallmedia command
local cmd
cmd="sudo '$installer_path/Contents/Resources/createinstallmedia' --volume /Volumes/$(get_codename $version_num)_dmg --nointeraction"
# For macOS 10.12, need to patch Info.plist
# For macOS 10.10-10.12, need to add --applicationpath
case "$version_num" in
10.10|10.11)
cmd="$cmd --applicationpath \"$installer_path\""
;;
10.12)
log_info "Patching Info.plist for Sierra compatibility..."
sudo /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString '12.6.03'" "$installer_path/Contents/Info.plist"
cmd="$cmd --applicationpath \"$installer_path\""
;;
esac
# Create install media
log_info "Running createinstallmedia..."
bash -c "$cmd"
sync && sleep 5
# Restore Info.plist on macOS 10.12
if [ "$version_num" == "10.12" ]; then
sudo /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString '12.6.06'" "/Applications/Install macOS Sierra.app/Contents/Info.plist"
sudo /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString '12.6.06'" "/Volumes/Install macOS Sierra/Install macOS Sierra.app/Contents/Info.plist"
fi
log_info "Starting DMG file size optimization..."
# Check minimum size requirements
MIN_SIZE=$(diskutil resizeVolume ${DISK_ID}s2 limits | grep "Minimum (constrained by file usage)" | sed 's/.*(\([0-9]*\) Bytes)/\1/')
# Add 50MB to $MIN_SIZE for macOS 10.1010.11 because the installation fails when resized to the minimum size
case "$version_num" in
10.10|10.11)
MIN_SIZE=$((MIN_SIZE + 52428800))
;;
esac
# Consolidate the installer data partition by shrinking the macOS installer partition to the minimum size.
log_info "Shrinking macOS installer partition to minimum..."
diskutil resizeVolume ${DISK_ID}s2 "${MIN_SIZE}B" free free 0 >/dev/null 2>&1
# Expand back to consolidate
log_info "Expanding back to consolidate data ..."
for attempt in $(seq 1 $RETRIES_COUNT); do
log_info "Resize attempt $attempt of $RETRIES_COUNT..."
if diskutil resizeVolume ${DISK_ID}s2 0 >/dev/null 2>&1; then
log_info "Partition resized successfully"
break
fi
if [ $attempt -lt $RETRIES_COUNT ]; then
log_warn "Resize failed, retrying..."
sync && sleep 5
fi
done
# Extract the exact version number and append it to the file name
local exact_version_number
exact_version_number=$(defaults read "/Volumes/$(get_installer_app_name "$version_num")/System/Library/CoreServices/SystemVersion" ProductVersion)
FINAL_OUTPUT_PATH="${output_file%.dmg}_$exact_version_number.dmg.img"
# Unmount
detach_disk "$DISK_ID" && DISK_ID=""
# Optimize DMG file size
log_info "Final optimization..."
sudo hdiutil resize -size min "$WORK_DIR/installer.dmg"
# Move to output and append .iso extension
mv -f "$WORK_DIR/installer.dmg" "$FINAL_OUTPUT_PATH"
}
# Main script
main() {
echo -e "${BOLD}${CYAN}"
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
echo "║ mkmaciso the ultimate tool for creating macOS installer ISO and DMG images ║"
echo "╚══════════════════════════════════════════════════════════════════════════════╝"
echo -e "${NC}"
# Check for availble disk space
check_disk_space
# Require sudo
check_sudo_access
# Validate image format (default to iso if empty, fallback to dmg if invalid input)
IMAGE_FORMAT=$(echo "$IMAGE_FORMAT" | tr '[:upper:]' '[:lower:]')
if [[ -z "$IMAGE_FORMAT" ]]; then
IMAGE_FORMAT="iso"
elif [[ "$IMAGE_FORMAT" != "iso" && "$IMAGE_FORMAT" != "dmg" ]]; then
log_warn "Invalid image format '$IMAGE_FORMAT'; falling back to dmg"
IMAGE_FORMAT="dmg"
fi
# Normalize version input
VERSION=$(echo "$VERSION" | tr '[:upper:]' '[:lower:]' | tr -d '_- ')
# Determine if input is a codename or version number
VERSION_NUM=""
VERSION_NAME=""
# Try as version number first, then as codename
if [[ "$VERSION" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
VERSION_NUM="$VERSION"
else
VERSION_NUM=$(get_version_number "$VERSION")
fi
# Get proper codename (also validates the version number)
VERSION_NAME=$(get_codename "$VERSION_NUM")
if [ -z "$VERSION_NAME" ]; then
log_info "Run with -h or --help for usage"
echo ""
log_error "Unknown macOS version: $VERSION"
log_info "Supported versions: 10.7, 10.8, 10.9, 10.10, 10.11, 10.12, 10.13, 10.14, 10.15, 11, 12, 13, 14, 15, 26"
log_info "Supported codenames: lion, mountain-lion, mavericks, yosemite, el-capitan, sierra, high-sierra, mojave, catalina, big-sur, monterey, ventura, sonoma, sequoia, tahoe"
exit 1
fi
log_info "Target macOS:${GREEN} $VERSION_NAME ($VERSION_NUM) ${NC}"
log_info "Image format:${GREEN} $(echo $IMAGE_FORMAT | tr '[:lower:]' '[:upper:]') ${NC}"
# Determine output filename if not specified
if [ -z "$OUTPUT_PATH" ]; then
OUTPUT_PATH="$(pwd)/macOS_${VERSION_NAME}.${IMAGE_FORMAT}"
else
# Ensure output path has correct extension
if [[ "$OUTPUT_PATH" != *.$IMAGE_FORMAT ]]; then
OUTPUT_PATH="${OUTPUT_PATH%.*}.$IMAGE_FORMAT"
fi
# Convert to absolute path
if [[ "$OUTPUT_PATH" != /* ]]; then
# Use dirname + basename to avoid "./" in the middle
OUTPUT_PATH="$(cd "$(dirname "$OUTPUT_PATH")" && pwd)/$(basename "$OUTPUT_PATH")"
fi
fi
log_info "Output path:${GREEN} $OUTPUT_PATH ${NC}"
echo ""
# Create work directory
WORK_DIR=$(mktemp -d -t LongQT-sea)
log_info "Working directory:${CYAN} $WORK_DIR ${NC}"
echo ""
# Check if installer already exists
INSTALLER_PATH="/Applications/$(get_installer_app_name "$VERSION_NUM").app"
if [ -n "$INSTALLER_PATH" ] && [ -d "$INSTALLER_PATH" ]; then
log_info "Found existing installer at:${CYAN} $INSTALLER_PATH ${NC}"
echo ""
else
log_info "Installer not found, downloading..."
# Download and install
if [[ "$VERSION_NUM" =~ ^10\.(7|8|9|10|11|12)$ ]]; then
# Legacy macOS versions
download_legacy_macos "$VERSION_NUM"
install_legacy_app "$VERSION_NUM" "$WORK_DIR/InstallMacOSX.dmg"
else
# Modern macOS (10.13 and later)
download_modern_macos "$VERSION_NUM" || exit 1
fi
if [ ! -d "$INSTALLER_PATH" ]; then
log_error "Failed to install macOS installer app"
exit 1
fi
fi
# Get volume name for the installer
VOLUME_NAME=$(get_installer_app_name "$VERSION_NUM")
if [ "$IMAGE_FORMAT" == "iso" ]; then
case "$VERSION_NUM" in
"10.7"|"10.8")
create_iso_dmg_10_7_10_8 "$INSTALLER_PATH" "$OUTPUT_PATH" "$VOLUME_NAME" "$VERSION_NUM" ;;
"10.9"|"10.10"|"10.11")
create_iso_10_9_10_11 "$INSTALLER_PATH" "$OUTPUT_PATH" "$VOLUME_NAME" "$VERSION_NUM" ;;
*)
create_iso_10_12_and_later "$INSTALLER_PATH" "$OUTPUT_PATH" "$VERSION_NUM" ;;
esac
else
case "$VERSION_NUM" in
"10.7"|"10.8")
create_iso_dmg_10_7_10_8 "$INSTALLER_PATH" "$OUTPUT_PATH" "$VOLUME_NAME" "$VERSION_NUM" ;;
"10.9")
create_dmg_10_9_to_10_12_alt_method "$INSTALLER_PATH" "$OUTPUT_PATH" "$VOLUME_NAME" "$VERSION_NUM" ;;
*)
create_dmg_10_10_and_later "$INSTALLER_PATH" "$OUTPUT_PATH" "$VERSION_NUM" ;;
esac
fi
# Final checks
if [ -f "$FINAL_OUTPUT_PATH" ]; then
FILE_SIZE_BYTES=$(stat -f%z "$FINAL_OUTPUT_PATH")
FILE_SIZE_GB=$(du -h "$FINAL_OUTPUT_PATH" | cut -f1)
# 4GB in bytes
MIN_SIZE=4294967296
if [ "$FILE_SIZE_BYTES" -lt "$MIN_SIZE" ]; then
log_error "Final image is smaller than 4GB"
log_error "The file is likely corrupted or incomplete"
log_warn "Remove corrupted file..."
rm -f "$FINAL_OUTPUT_PATH"
exit 1
fi
echo ""
log_info "Successfully created installer image!"
log_info "File: $FINAL_OUTPUT_PATH"
log_info "Size: $FILE_SIZE_GB"
echo ""
if [ "$IMAGE_FORMAT" == "iso" ]; then
log_info "This ISO image is intended for creating macOS virtual machine:"
log_info " - Compatible with Proxmox VE, QEMU, VMware, and VirtualBox"
log_info " - Add as DVD/CD drive to the VM"
echo ""
else
log_info "This DMG image is intended for:"
log_info " - Flash to USB drive using Rufus (Windows) or dd (Linux)"
log_info " - Can also be used with virtual machines (requires conversion to .vhd for Hyper-V or .vmdk for VMware)"
log_info " - Add to the VM as a virtual hard drive"
echo ""
fi
else
log_error "Failed to create macOS_${VERSION_NAME}.${IMAGE_FORMAT} image"
exit 1
fi
}
# Show usage if -h or --help is passed
if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then
cat << 'EOF'
╔══════════════════════════════════════════════════════════════════════════════╗"
║ mkmaciso the ultimate tool for creating macOS installer ISO and DMG images ║"
╚══════════════════════════════════════════════════════════════════════════════╝"
Usage:
mkmaciso [VERSION] [FORMAT] [OUTPUT_PATH]
mkmaciso # Interactive mode
Parameters:
VERSION macOS version number or codename, e.g. 26 or Tahoe
FORMAT iso or dmg (default: iso)
OUTPUT_PATH Output file path (default: ./macOS_<version>.<format>)
Supported versions:
10.7 Lion 10.14 Mojave
10.8 Mountain Lion 10.15 Catalina
10.9 Mavericks 11 Big Sur
10.10 Yosemite 12 Monterey
10.11 El Capitan 13 Ventura
10.12 Sierra 14 Sonoma
10.13 High Sierra 15 Sequoia
26 Tahoe
Examples:
mkmaciso Interactive mode
mkmaciso 14 Create Sonoma ISO
mkmaciso 14 dmg Create Sonoma DMG
mkmaciso sonoma dmg ~/Desktop/out.dmg Create Sonoma DMG at specified path
Requirements:
- macOS 11 (Big Sur) or later for create macOS 10.13+ image
- Internet connection for downloading installers
- Sufficient disk space (~20-40GB temporary, final size varies)
- Administrator privileges (sudo)
- For best results, run this on Intel Macs
EOF
exit 0
fi
# Check if running in interactive mode with piped (no parameters)
if [ $# -eq 0 ]; then
if [ -p /dev/stdin ] || [ ! -t 0 ]; then
clear
log_error "Interactive mode unavailable when piped"
log_info "Usage: curl -sL <URL> | bash -s <VERSION> [FORMAT] [OUTPUT]"
echo ""
log_info "Example: curl -sL <URL> | bash -s 26"
log_info "Example: curl -sL <URL> | bash -s 26 dmg"
log_info "Example: curl -sL <URL> | bash -s 26 iso ~/Desktop/Tahoe.iso"
exit 1
fi
run_interactive_menu
fi
# Run main function
main