Skip to content

Instantly share code, notes, and snippets.

@efimovalex
Forked from jetfir3/download_fusion.sh
Created October 30, 2026 15:56
Show Gist options
  • Select an option

  • Save efimovalex/41b7cde87d505207cd75b832ab8defc8 to your computer and use it in GitHub Desktop.

Select an option

Save efimovalex/41b7cde87d505207cd75b832ab8defc8 to your computer and use it in GitHub Desktop.
Download VMware Fusion Pro Without a Broadcom Account
#!/bin/bash
# Download VMware Fusion DMGs from a third-party Internet Archive collection.
# Compatible with macOS's Bash 3.2. No Homebrew, jq, or Python required.
set -euo pipefail
export LC_ALL=C
ARCHIVE_ITEM="vmwareworkstationarchive"
DOWNLOAD_DIR="${HOME}/Downloads"
VERSION=""
MODE=""
PRINT_URL=false
TMP_DIR=""
LOCK_DIR=""
usage() {
cat <<'EOF'
Usage: download_fusion.sh [options]
With no selection option, show an interactive version menu.
-l, --list List archived versions, newest first
--latest Download the newest archived version
-v, --version VERSION Download an exact version (13.6.4, 25H2, 26H1u1)
-d, --directory DIR Destination directory (default: ~/Downloads)
--url Print the selected URL without downloading
--item IDENTIFIER Use another Internet Archive collection
-h, --help Show this help
Examples:
bash download_fusion.sh --latest
bash download_fusion.sh -v 25H2u1
bash download_fusion.sh --latest --url
Downloads are verified against the archive's file size and SHA-1 checksum.
This checks agreement with the archive, not publisher authenticity.
The script only downloads; it does not install or open the disk image.
After verification, it clears the DMG's extended attributes with xattr -c.
EOF
}
die() { printf 'Error: %s\n' "$*" >&2; exit 1; }
log() { printf '%s\n' "$*" >&2; }
cleanup() {
if [[ -n "$LOCK_DIR" ]]; then rmdir "$LOCK_DIR" 2>/dev/null || true; fi
if [[ -n "$TMP_DIR" ]]; then rm -rf "$TMP_DIR"; fi
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
set_mode() {
[[ -z "$MODE" ]] || die 'Use only one of --list, --latest, or --version.'
MODE="$1"
}
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-l|--list) set_mode list; shift ;;
--latest) set_mode latest; shift ;;
-v|--version)
[[ $# -ge 2 && -n "$2" && "$2" != -* ]] || die "$1 requires a version."
set_mode version; VERSION="$2"; shift 2 ;;
-d|--directory)
[[ $# -ge 2 && -n "$2" && "$2" != -* ]] || die "$1 requires a directory."
DOWNLOAD_DIR="$2"; shift 2 ;;
--item)
[[ $# -ge 2 && -n "$2" && "$2" != -* ]] || die '--item requires an identifier.'
ARCHIVE_ITEM="$2"; shift 2 ;;
--url) PRINT_URL=true; shift ;;
-s|-k) die 'The retired source/compression options were removed. Use --help for current options.' ;;
*) die "Unknown argument: $1. Use --help for usage." ;;
esac
done
[[ "$MODE" != list || "$PRINT_URL" == false ]] || die '--url requires a version selection, not --list.'
[[ "$ARCHIVE_ITEM" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || die 'Invalid archive item identifier.'
if [[ -n "$VERSION" ]]; then
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ || "$VERSION" =~ ^[0-9]{2}[Hh][12]([Uu][0-9]+)?$ ]] ||
die 'Use a version such as 13.6.4, 25H2, or 26H1u1.'
fi
[[ "$(uname -s)" == Darwin ]] || die 'This script uses built-in macOS tools and must run on macOS.'
for tool in curl osascript shasum stat awk mktemp mkdir rmdir ln rm sleep tr xattr; do
command -v "$tool" >/dev/null 2>&1 || die "Required tool not found: $tool"
done
TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/fusion-download.XXXXXXXX")
log "Fetching available versions from archive.org ($ARCHIVE_ITEM)..."
curl -q --fail --silent --show-error --location \
--proto '=https' --proto-redir '=https' --connect-timeout 20 \
--max-time 120 --retry 2 --retry-delay 2 \
--output "$TMP_DIR/metadata.json" "https://archive.org/metadata/$ARCHIVE_ITEM" ||
die 'Could not fetch archive metadata. Check your connection or try again later.'
osascript -l JavaScript - "$TMP_DIR/metadata.json" "$ARCHIVE_ITEM" > "$TMP_DIR/catalog.tsv" <<'JXA' || die 'Could not read archive metadata.'
ObjC.import('Foundation');
function run(args) {
var text = $.NSString.stringWithContentsOfFileEncodingError(args[0], $.NSUTF8StringEncoding, null);
if (!text) throw new Error('Cannot read metadata file.');
var data = JSON.parse(ObjC.unwrap(text));
if (!Array.isArray(data.files)) throw new Error('Archive item has no file list.');
var rows = [];
data.files.forEach(function (f) {
if (typeof f.name !== 'string') return;
var parts = f.name.split('/');
if (parts.some(function (p) { return !p || p === '.' || p === '..' || /[\x00-\x1f\x7f]/.test(p); })) return;
if (parts.length > 1 && parts[0] !== 'Fusion') return;
var filename = parts[parts.length - 1];
var m = /^VMware-Fusion-(\d+\.\d+\.\d+|\d{2}H[12](?:u\d+)?)-(\d+)(?:[_-](universal|x86|x86_64|arm64))?\.dmg$/i.exec(filename);
if (!m || (f.private && String(f.private) !== 'false')) return;
var version = m[1].replace(/h/i, 'H').replace(/u/i, 'u');
var cal = /^(\d{2})H([12])(?:u(\d+))?$/.exec(version);
var numeric = version.split('.').map(Number);
var arch = (m[3] || '').toLowerCase();
if (!arch) {
if (cal || numeric[0] >= 13) return;
arch = 'intel';
}
if (arch === 'x86' || arch === 'x86_64') arch = 'intel';
var size = String(f.size), sha1 = String(f.sha1 || '').toLowerCase();
if (!/^[1-9]\d*$/.test(size) || Number(size) > 9007199254740991 || !/^[a-f0-9]{40}$/.test(sha1))
throw new Error('Missing or invalid size/checksum for ' + filename);
var url = 'https://archive.org/download/' + args[1] + '/' + parts.map(encodeURIComponent).join('/');
var key = cal ? [1, +cal[1], +cal[2], +(cal[3] || 0), 0] : [0, numeric[0], numeric[1], numeric[2], 0];
key.push(Number(m[2]));
rows.push({key: key, fields: [version, m[2], arch, size, sha1, filename, url]});
});
rows.sort(function (a, b) {
for (var i = 0; i < a.key.length; i++) {
if (a.key[i] !== b.key[i]) return b.key[i] - a.key[i];
}
if (a.fields[2] === 'universal' && b.fields[2] !== 'universal') return -1;
if (b.fields[2] === 'universal' && a.fields[2] !== 'universal') return 1;
return a.fields[6] < b.fields[6] ? -1 : a.fields[6] > b.fields[6] ? 1 : 0;
});
return rows.map(function (r) { return r.fields.join('\t'); }).join('\n');
}
JXA
VERSIONS=(); BUILDS=(); ARCHES=(); SIZES=(); HASHES=(); FILENAMES=(); URLS=()
while IFS=$'\t' read -r ver build arch size sha1 filename url; do
[[ -n "$ver" ]] || continue
VERSIONS+=("$ver"); BUILDS+=("$build"); ARCHES+=("$arch"); SIZES+=("$size")
HASHES+=("$sha1"); FILENAMES+=("$filename"); URLS+=("$url")
done < "$TMP_DIR/catalog.tsv"
[[ ${#VERSIONS[@]} -gt 0 ]] || die 'No supported Fusion DMGs found in this archive item.'
show_versions() {
local i
printf '%4s %-10s %-10s %-10s %s\n' '#' 'VERSION' 'BUILD' 'ARCH' 'MiB'
for ((i=0; i<${#VERSIONS[@]}; i++)); do
printf '%4d %-10s %-10s %-10s %s\n' "$((i+1))" "${VERSIONS[$i]}" "${BUILDS[$i]}" "${ARCHES[$i]}" \
"$(awk -v size="${SIZES[$i]}" 'BEGIN { printf "%.1f", size/1048576 }')"
done
}
if [[ "$MODE" == list ]]; then show_versions; exit 0; fi
SELECTED=-1
HOST_ARCH=$(uname -m)
for ((i=0; i<${#VERSIONS[@]}; i++)); do
if [[ "$MODE" == version ]]; then
if [[ "$(printf '%s' "${VERSIONS[$i]}" | tr '[:upper:]' '[:lower:]')" != "$(printf '%s' "$VERSION" | tr '[:upper:]' '[:lower:]')" ]]; then continue; fi
elif [[ "$MODE" != latest ]]; then
break
fi
if [[ "${ARCHES[$i]}" == universal || ("$HOST_ARCH" == arm64 && "${ARCHES[$i]}" == arm64) ||
("$HOST_ARCH" == x86_64 && "${ARCHES[$i]}" == intel) ]]; then
SELECTED=$i; break
fi
done
if [[ "$MODE" == version && "$SELECTED" -lt 0 ]]; then
die "Version $VERSION is unavailable for this Mac's architecture. Use --list to see archived versions."
elif [[ "$MODE" == latest && "$SELECTED" -lt 0 ]]; then
die "No archived installer matches this Mac's architecture. Use --list to inspect the archive."
elif [[ -z "$MODE" ]]; then
[[ -t 0 ]] || die 'Use --latest, --version, or --list when running noninteractively.'
show_versions >&2
while true; do
printf 'Choose an installer number (or q to quit): ' >&2
read -r choice || exit 0
[[ "$choice" != q && "$choice" != Q ]] || exit 0
if [[ "$choice" =~ ^[0-9]{1,6}$ ]]; then
choice=$((10#$choice))
if [[ "$choice" -ge 1 && "$choice" -le ${#VERSIONS[@]} ]]; then
SELECTED=$((choice-1)); break
fi
fi
log 'Choose a number from the list.'
done
fi
URL="${URLS[$SELECTED]}"
if $PRINT_URL; then printf '%s\n' "$URL"; exit 0; fi
if [[ ("$HOST_ARCH" == arm64 && "${ARCHES[$SELECTED]}" == intel) ||
("$HOST_ARCH" == x86_64 && "${ARCHES[$SELECTED]}" == arm64) ]]; then
log 'Notice: This historic installer is for a different CPU architecture.'
fi
log "Selected Fusion ${VERSIONS[$SELECTED]} (build ${BUILDS[$SELECTED]}, ${ARCHES[$SELECTED]})."
log 'Source: a third-party archive. Checksums establish agreement with the archive only.'
mkdir -p "$DOWNLOAD_DIR"
DOWNLOAD_DIR=$(cd "$DOWNLOAD_DIR" && pwd -P)
OUTPUT="$DOWNLOAD_DIR/${FILENAMES[$SELECTED]}"
PARTIAL="$OUTPUT.part"
SIZE="${SIZES[$SELECTED]}"
SHA1="${HASHES[$SELECTED]}"
if ! mkdir "$OUTPUT.lock" 2>/dev/null; then
die "Another download may be running. If none is running, remove $OUTPUT.lock and retry."
fi
LOCK_DIR="$OUTPUT.lock"
for path in "$OUTPUT" "$PARTIAL"; do
[[ ! -L "$path" ]] || die "Refusing a symbolic link: $path"
[[ ! -e "$path" || -f "$path" ]] || die "Destination is not a regular file: $path"
done
verify_file() {
local bytes digest
bytes=$(stat -f '%z' "$1") || return 1
[[ "$bytes" == "$SIZE" ]] || return 1
digest=$(shasum -a 1 < "$1") || return 1
[[ "${digest%% *}" == "$SHA1" ]]
}
clear_attributes() {
log 'Clearing extended attributes from the verified DMG...'
xattr -c "$OUTPUT" || die "Could not clear extended attributes. Verified installer kept at: $OUTPUT"
}
if [[ -f "$OUTPUT" ]]; then
log 'Checking the existing installer...'
verify_file "$OUTPUT" || die "Existing file does not match archive metadata. Move it aside before retrying: $OUTPUT"
log 'Existing installer matches archive size and SHA-1.'
clear_attributes
printf '%s\n' "$OUTPUT"
exit 0
fi
bytes=0
if [[ -f "$PARTIAL" ]]; then bytes=$(stat -f '%z' "$PARTIAL"); fi
[[ "$bytes" -le "$SIZE" ]] || die "Partial file is too large. Move it aside before retrying: $PARTIAL"
if [[ "$bytes" -lt "$SIZE" ]]; then
for attempt in 1 2 3; do
log "Downloading (attempt $attempt of 3); interrupted downloads resume on the next run."
if curl -q --fail --show-error --location --progress-bar \
--proto '=https' --proto-redir '=https' --connect-timeout 20 \
--speed-limit 1024 --speed-time 120 --continue-at - \
--output "$PARTIAL" "$URL"; then
break
else
status=$?
[[ "$status" != 33 ]] || die "The server refused download resumption. Move $PARTIAL aside and retry."
[[ "$attempt" -lt 3 ]] || die "Download failed. Partial file kept at: $PARTIAL"
sleep 3
fi
done
fi
log 'Verifying file size and SHA-1...'
if ! verify_file "$PARTIAL"; then
rm -f "$PARTIAL"
die 'Downloaded file failed verification. The corrupt partial was removed; rerun to download again.'
fi
ln "$PARTIAL" "$OUTPUT" || die "Could not publish the download. Verified partial kept at: $PARTIAL"
rm -f "$PARTIAL"
log 'Download verified against archive metadata.'
clear_attributes
printf '%s\n' "$OUTPUT"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment