Created
December 25, 2025 01:47
-
-
Save dangayle/82ffaa200b700a77b69fe5db00bcd191 to your computer and use it in GitHub Desktop.
macOS bulk installer for audio plugins, etc.
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 | |
| # Global variables for cleanup | |
| MOUNT_POINTS=() | |
| TEMP_DIRS=() | |
| INSTALLED=() | |
| FAILED=() | |
| # Verbose flag (default is quiet). Use `--verbose` or `-v` to enable. | |
| VERBOSE=0 | |
| # Helper: verbose-only echo | |
| v_echo() { | |
| if [ "$VERBOSE" -eq 1 ]; then | |
| echo "$@" | |
| fi | |
| } | |
| # Cleanup function | |
| cleanup() { | |
| v_echo "Cleaning up..." | |
| # Unmount any mounted .dmg files | |
| for mount_point in "${MOUNT_POINTS[@]}"; do | |
| v_echo "Unmounting $mount_point..." | |
| hdiutil detach "$mount_point" >/dev/null 2>&1 || v_echo "Failed to unmount $mount_point" | |
| done | |
| # Remove any temporary directories | |
| for temp_dir in "${TEMP_DIRS[@]}"; do | |
| v_echo "Removing temporary directory $temp_dir..." | |
| rm -rf "$temp_dir" | |
| done | |
| v_echo "Cleanup complete. Exiting." | |
| exit 1 | |
| } | |
| # Trap SIGINT (Command+C) and call cleanup | |
| trap cleanup SIGINT | |
| # Parse args (allow `--verbose` / `-v` and optional base dir) | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --verbose|-v) | |
| VERBOSE=1 | |
| shift | |
| ;; | |
| *) | |
| base_dir="$1" | |
| shift | |
| ;; | |
| esac | |
| done | |
| # Function to install .pkg files | |
| install_pkg() { | |
| local pkg_file="$1" | |
| echo "Installing $pkg_file..." | |
| if sudo installer -pkg "$pkg_file" -target /; then | |
| INSTALLED+=("$pkg_file") | |
| else | |
| FAILED+=("$pkg_file") | |
| fi | |
| } | |
| # Function to process standalone installers | |
| process_standalone_installers() { | |
| local dir="$1" | |
| v_echo "Looking for standalone installers in $dir..." | |
| # Collect candidate files: executable bit or inside an app's Contents/MacOS | |
| mapfile -t candidates < <(find "$dir" -type f \( -perm -111 -o -path '*/Contents/MacOS/*' \) 2>/dev/null) | |
| for exec_file in "${candidates[@]}"; do | |
| # Skip helper scripts like installbuilder.sh | |
| if [[ "$exec_file" == *installbuilder.sh ]]; then | |
| v_echo "Skipping helper script: $exec_file" | |
| continue | |
| fi | |
| # Skip obvious resource files by extension | |
| case "$exec_file" in | |
| *.png|*.jpg|*.jpeg|*.gif|*.bmp|*.tiff|*.ico|*.plist|*.lproj/*|*.txt|*.md|*.html|*.css|*.pdf) | |
| v_echo "Skipping resource file: $exec_file" | |
| continue | |
| ;; | |
| esac | |
| # Use `file` to classify the candidate and skip non-executables | |
| if ! file_out=$(file -b --mime-encoding --mime-type "$exec_file" 2>/dev/null); then | |
| v_echo "Unable to determine file type for $exec_file; skipping" | |
| continue | |
| fi | |
| # Disallow common image/text mime types | |
| if echo "$file_out" | grep -qiE 'image/|text/html|text/css|application/xml|application/json'; then | |
| v_echo "Skipping non-executable mime type ($file_out): $exec_file" | |
| continue | |
| fi | |
| # For plain text we require a shebang to treat it as executable script | |
| if echo "$file_out" | grep -qi '^text/' ; then | |
| first_line=$(head -n1 "$exec_file" 2>/dev/null || true) | |
| if [[ "$first_line" != "#!"* ]]; then | |
| v_echo "Skipping text file without shebang: $exec_file" | |
| continue | |
| fi | |
| fi | |
| # Secondary human-readable `file` check | |
| human_desc=$(file -b "$exec_file" 2>/dev/null || true) | |
| if ! echo "$human_desc" | grep -qiE 'Mach-O|executable|shell script|Python script|Perl script|Bourne-Again shell script|script'; then | |
| v_echo "Skipping (not runnable): $exec_file ($human_desc)" | |
| continue | |
| fi | |
| v_echo "Candidate installer: $exec_file (type: $human_desc)" | |
| # Prefer to run binaries inside an app bundle's Contents/MacOS | |
| if [[ "$exec_file" == */Contents/MacOS/* ]]; then | |
| if [ "$VERBOSE" -eq 1 ]; then | |
| echo "Running app-bundle executable: $exec_file" | |
| fi | |
| if sudo "$exec_file" --mode unattended --unattendedmodeui none 2>/dev/null; then | |
| INSTALLED+=("$exec_file") | |
| else | |
| FAILED+=("$exec_file") | |
| fi | |
| continue | |
| fi | |
| # InstallBuilder unattended attempt | |
| if strings "$exec_file" 2>/dev/null | grep -qi 'InstallBuilder'; then | |
| v_echo "Detected InstallBuilder binary: $exec_file (attempting unattended)" | |
| if sudo "$exec_file" --mode unattended --unattendedmodeui none 2>/dev/null; then | |
| INSTALLED+=("$exec_file") | |
| else | |
| FAILED+=("$exec_file") | |
| fi | |
| continue | |
| fi | |
| # Otherwise, only attempt to run if the file is clearly an executable binary | |
| if echo "$human_desc" | grep -qiE 'Mach-O|executable'; then | |
| v_echo "Running executable: $exec_file" | |
| if sudo "$exec_file" 2>/dev/null; then | |
| INSTALLED+=("$exec_file") | |
| else | |
| FAILED+=("$exec_file") | |
| fi | |
| else | |
| v_echo "Skipping $exec_file (not a runnable binary)" | |
| fi | |
| done | |
| } | |
| # Helper: attempt to mount a dmg and return the mount point (or empty string on failure) | |
| # Note: simplified DMG mount helper removed — use direct hdiutil attach in process_dmg | |
| # Function to process .dmg files | |
| process_dmg() { | |
| local dmg_file="$1" | |
| dmg_file=$(realpath "$dmg_file") # Normalize the path | |
| v_echo "Processing $dmg_file..." | |
| # Attach using plist output so we can reliably extract device and mount-point | |
| local plist_out | |
| plist_out=$(hdiutil attach -plist -nobrowse "$dmg_file" 2>/dev/null) || plist_out="" | |
| local dev | |
| local mount_point | |
| if command -v xmllint >/dev/null 2>&1 && [ -n "$plist_out" ]; then | |
| dev=$(echo "$plist_out" | xmllint --xpath 'string(//key[. = "dev-entry"]/following-sibling::string[1])' - 2>/dev/null || true) | |
| mount_point=$(echo "$plist_out" | xmllint --xpath 'string(//key[. = "mount-point"]/following-sibling::string[1])' - 2>/dev/null || true) | |
| else | |
| # Fallback to plain attach output | |
| local attach_out | |
| attach_out=$(hdiutil attach -nobrowse "$dmg_file" 2>&1) | |
| # try to pull mount path (first /Volumes/... occurrence) | |
| mount_point=$(echo "$attach_out" | sed -n 's/.*\(/Volumes\/[^ ]*\).*/\1/p' | head -n1) | |
| # try to pull device (first dev entry like /dev/diskXsY) | |
| dev=$(echo "$attach_out" | awk '/\/dev\//{for(i=1;i<=NF;i++) if ($i ~ /^\/dev\//) print $i; exit}' ) | |
| fi | |
| # If we couldn't determine mount_point yet, try a quick parse of non-plist output | |
| if [ -z "$mount_point" ]; then | |
| mount_point=$(hdiutil info | awk '/\/Volumes\//{for(i=1;i<=NF;i++) if ($i ~ /^\/Volumes\//) print $i}' | head -n1) | |
| fi | |
| if [ -z "$dev" ] && [ -n "$mount_point" ]; then | |
| # try to find dev from mount table | |
| dev=$(mount | awk -v mp="$mount_point" '$3==mp {print $1; exit}') || true | |
| fi | |
| # Wait briefly for filesystem to appear if mount_point exists but is not accessible yet | |
| if [ -n "$mount_point" ]; then | |
| local retries=0 | |
| while [ $retries -lt 5 ] && [ ! -d "$mount_point" ]; do | |
| sleep 0.2 | |
| retries=$((retries+1)) | |
| done | |
| fi | |
| if [ -z "$mount_point" ] || [ ! -d "$mount_point" ]; then | |
| v_echo "Failed to mount $dmg_file (mountpoint not found)" | |
| FAILED+=("$dmg_file") | |
| # if we have a device value, try to detach it | |
| if [ -n "$dev" ]; then | |
| hdiutil detach "$dev" >/dev/null 2>&1 || true | |
| fi | |
| return 1 | |
| fi | |
| v_echo "Mounted $dmg_file at $mount_point" | |
| # store the device entry (or mount_point) for cleanup/detach | |
| if [ -n "$dev" ]; then | |
| MOUNT_POINTS+=("$dev") | |
| else | |
| MOUNT_POINTS+=("$mount_point") | |
| fi | |
| # Process .pkg files inside the mounted .dmg (only regular files) | |
| find "$mount_point" -name "*.pkg" -type f | while read pkg_file; do | |
| install_pkg "$pkg_file" | |
| done | |
| # Process standalone installers inside the mounted .dmg | |
| process_standalone_installers "$mount_point" | |
| # Unmount the .dmg by device if we have it, otherwise by mount point | |
| v_echo "Unmounting $dmg_file..." | |
| if [ -n "$dev" ]; then | |
| hdiutil detach "$dev" >/dev/null 2>&1 || v_echo "Failed to detach $dev" | |
| # remove from tracking array | |
| MOUNT_POINTS=("${MOUNT_POINTS[@]/$dev}") | |
| else | |
| hdiutil detach "$mount_point" >/dev/null 2>&1 || v_echo "Failed to detach $mount_point" | |
| MOUNT_POINTS=("${MOUNT_POINTS[@]/$mount_point}") | |
| fi | |
| return 0 | |
| } | |
| # Function to process .zip files | |
| process_zip() { | |
| local zip_file="$1" | |
| v_echo "Processing $zip_file..." | |
| # Create a temporary directory for extraction | |
| local temp_dir | |
| temp_dir=$(mktemp -d) | |
| TEMP_DIRS+=("$temp_dir") # Track temp directory for cleanup | |
| unzip -q "$zip_file" -d "$temp_dir" | |
| # Recursively process extracted files | |
| process_directory "$temp_dir" | |
| # Clean up the temporary directory | |
| rm -rf "$temp_dir" | |
| TEMP_DIRS=("${TEMP_DIRS[@]/$temp_dir}") # Remove from tracked temp directories | |
| } | |
| # Recursive function to process directories | |
| process_directory() { | |
| local dir="$1" | |
| v_echo "Processing directory $dir..." | |
| # Process .pkg files | |
| find "$dir" -name "*.pkg" | while read pkg_file; do | |
| install_pkg "$pkg_file" | |
| done | |
| # Process .dmg files | |
| find "$dir" -name "*.dmg" | while read dmg_file; do | |
| process_dmg "$dmg_file" | |
| done | |
| # Process .zip files | |
| find "$dir" -name "*.zip" | while read zip_file; do | |
| process_zip "$zip_file" | |
| done | |
| } | |
| # Main script execution | |
| # The directory to process must be provided as the first positional argument | |
| if [ -z "$base_dir" ]; then | |
| echo "Usage: $0 [--verbose|-v] <directory>" | |
| exit 2 | |
| fi | |
| # Ensure directory exists and expand to absolute path | |
| if [ ! -d "$base_dir" ]; then | |
| echo "Error: directory '$base_dir' does not exist" | |
| exit 2 | |
| fi | |
| base_dir="$(cd "$base_dir" && pwd)" | |
| process_directory "$base_dir" | |
| # Final summary: either Done or list items that failed | |
| if [ ${#FAILED[@]} -ne 0 ]; then | |
| echo | |
| echo "Failed installs/mounts:" | |
| for f in "${FAILED[@]}"; do | |
| echo " - $f" | |
| done | |
| exit 1 | |
| else | |
| echo "Done." | |
| fi |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This might be the most useful script I've ever written. Any time I change computers or update my OS, I have to re-install all my audio plugins and stuff, and it's a pain going through the all the installers one by one. With this script, I toss the
.dmg,.zip, or.exefiles into one directory, typeinstall_packages.sh ~/Downloads/<plugin directory>and it does the business.Obviously, don't use this with installers you don't trust.