Skip to content

Instantly share code, notes, and snippets.

@dangayle
Created December 25, 2025 01:47
Show Gist options
  • Select an option

  • Save dangayle/82ffaa200b700a77b69fe5db00bcd191 to your computer and use it in GitHub Desktop.

Select an option

Save dangayle/82ffaa200b700a77b69fe5db00bcd191 to your computer and use it in GitHub Desktop.
macOS bulk installer for audio plugins, etc.
#!/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
@dangayle
Copy link
Author

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 .exe files into one directory, type install_packages.sh ~/Downloads/<plugin directory> and it does the business.

Obviously, don't use this with installers you don't trust.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment