-
-
Save efimovalex/41b7cde87d505207cd75b832ab8defc8 to your computer and use it in GitHub Desktop.
Download VMware Fusion Pro Without a Broadcom Account
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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