#!/bin/sh
set -Eeo pipefail
# A path to the template DMG file TEMPLATE_DMG= # Source file to add to DMG SOURCE_FILE= # A path Output DMG OUTPUT_DMG= # Codesign identity if passed CODE_SIGN_IDENTITY= # hdiutil verbosity level HDIUTIL_VERBOSITY=
# Mount point - random UUID to avoid conflicts DMG_MOUNT_POINT=$(uuidgen) # A path to a temporary (writable) DMG file DMG_TEMP=“$(mktemp).dmg”
# Maximum attempts to perform before trying force DMG detachment. MAXIMUM_UNMOUNTING_ATTEMPTS=3
# Outputs script usage function usage() {
cat <<EOHELP
Updates file or folder in template DMG file.
Usage: $(basename $0) [options] –source <source_folder> –template_dmg <template.dmg> –output_dmg <output_name.dmg>
The <source_folder> will be copied into the disk image.
Options:
--source Source file or folder to add into the new DMG. --template-dmg Path to the template disk image file. --output-dmg Path to the resulting disk image file. --code-sign-identity Code sign idenity to sign the resulting DMG. --hdiutil-verbose Execute hdiutil in verbose mode. --hdiutil-quiet Execute hdiutil in quiet mode. -h, --help display this help screen
EOHELP
exit 0
}
# Detaches mounted DMG function unmount() {
DEV_NAME=$1 DMG_MOUNT_POINT=$2 if [[ ! -z "${DMG_MOUNT_POINT}" && ! -d "${DMG_MOUNT_POINT}" ]]; then # DMG is not mounted break fi # Unmount unmounting_attempts=0 until echo "⏏️ Unmount '${DEV_NAME}'" (( unmounting_attempts++ )) hdiutil detach "${DEV_NAME}" exit_code=$? (( exit_code == 0 )) && break # nothing goes wrong (( exit_code != 16 )) && exit $exit_code # exit with the original exit code # The above statement returns 1 if test failed (exit_code == 16). # It can make the code in the {do... done} block to be executed do (( unmounting_attempts == MAXIMUM_UNMOUNTING_ATTEMPTS )) && exit 16 # patience exhausted, exit with code EBUSY echo "🚦 Wait a moment..." sleep $(( 1 * (2 ** unmounting_attempts) )) done unset unmounting_attempts if [[ ! -z "${DMG_MOUNT_POINT}" && -d "${DMG_MOUNT_POINT}" ]]; then echo " ⏏️ Unmount '${DEV_NAME}' at '${DMG_MOUNT_POINT}' with force" hdiutil detach "${DEV_NAME}" -force fi
}
# Check if min parameters count is set. if [[ -z “$6” ]]; then
echo "🙅 Not enough arguments. Run '$(basename $0) --help' for help." exit 1
fi
# Argument parsing. while [[ “${1:0:1}” = “-” ]]; do
case $1 in --template-dmg) TEMPLATE_DMG="$2" shift; shift;; --source) SOURCE_FILE="$2" shift; shift;; --output-dmg) OUTPUT_DMG="$2" shift; shift;; --code-sign-identity) CODE_SIGN_IDENTITY="$2" shift; shift;; --hdiutil-verbose) HDIUTIL_VERBOSITY='-verbose' shift;; --hdiutil-quiet) HDIUTIL_VERBOSITY='-quiet' shift;; --help) usage;; -*) echo "🤨 Unknown option: $1. Run 'create-dmg --help' for help." exit 1;; esac
done
# Check codesing. CODE_SIGN_FINGERPRINT_ID= if [[ ! -z “${CODE_SIGN_IDENTITY}” ]]; then
CODE_SIGN_FINGERPRINT_ID=$(security find-identity -v -p codesigning | grep -E "${CODE_SIGN_IDENTITY}" | awk '{ print $2; exit }') if [[ -z "${CODE_SIGN_FINGERPRINT_ID}" ]]; then echo "❌ Codesign identity '${CODE_SIGN_IDENTITY}' was not found in the Keychain. Import the codesign certificate '${CODE_SIGN_IDENTITY}' with private key into your Keychain and try again." exit 1 fi
fi
# Returns dev/ name from mounting point. function get_device_name {
DMG_MOUNT_POINT=$1 DEV_NAME=$(hdiutil info | egrep '^/dev/' | grep "$DMG_MOUNT_POINT" | sed 1q | awk '{ print $1 }' || echo '') echo "${DEV_NAME}"
}
# Remove temp DMG if any. if [[ -f “${DMG_TEMP}” ]]; then
echo "🧹 Clean-up" rm -Rf "${DMG_TEMP}"
fi
# Convert DMG template to RW format. echo “🔄 Convert template ‘${TEMPLATE_DMG}’ to writable disk image at ‘${DMG_TEMP}’” hdiutil convert “${TEMPLATE_DMG}” -format UDRW -o “${DMG_TEMP}” ${HDIUTIL_VERBOSITY}
# Calculate max resulting image size. SOURCE_SIZE=$(du -sm “${SOURCE_FILE}” | awk ‘{ print $1 }’) TEMPLATE_SIZE=$(du -sm “${TEMPLATE_DMG}” | awk ‘{ print $1 }’) DISK_IMAGE_SIZE=$(expr $SOURCE_SIZE ‘+’ $SOURCE_SIZE ‘*’ 10 ‘/’ 100 ‘+’ 10 + $TEMPLATE_SIZE) # Increase actual size by 10% and add extra 10MB on top. echo “↔️ Resize ‘${DMG_TEMP}’ to ${DISK_IMAGE_SIZE}MB” hdiutil resize “${DMG_TEMP}” -size ${DISK_IMAGE_SIZE}m ${HDIUTIL_VERBOSITY}
# Try unmount dmg if it was mounted previously. DEV_NAME=$(get_device_name “${DMG_MOUNT_POINT}”) if [[ ! -z “${DEV_NAME}” ]]; then
unmount "${DEV_NAME}" "${DMG_MOUNT_POINT}"
fi
# Mount writable disk image. echo “🧗 Mount ‘${DMG_TEMP}’ at ${DMG_MOUNT_POINT}” hdiutil attach -readwrite -noverify -noautoopen -nobrowse “${DMG_TEMP}” -mountpoint “${DMG_MOUNT_POINT}” ${HDIUTIL_VERBOSITY} DEV_NAME=$(get_device_name “${DMG_MOUNT_POINT}”) echo “💾 Device name: ${DEV_NAME}”
# Copy source to DMG. echo “♊️ Copy ‘${SOURCE_FILE}’ to ‘${DMG_MOUNT_POINT}’” DESTINATION_FILE=“${DMG_MOUNT_POINT}”/$(basename – “${SOURCE_FILE}”) if [[ -f “${DESTINATION_FILE}” || -d “${DESTINATION_FILE}” ]]; then
echo "🧹 Remove existing ${DESTINATION_FILE}" rm -rf "${DESTINATION_FILE}"
fi ditto –rsrc –extattr “${SOURCE_FILE}” “${DMG_MOUNT_POINT}”/$(basename – “${SOURCE_FILE}”)
# Flush disk cache to prevent “Resource busy” failures. sync
# Unmount updated disk image. unmount “${DEV_NAME}” “${DMG_MOUNT_POINT}”
# Convert DMG to compressed RO format. if [[ -f “${OUTPUT_DMG}” ]]; then
echo "🧹 Remove existing ${OUTPUT_DMG}" rm -Rf "${OUTPUT_DMG}"
fi hdiutil convert “${DMG_TEMP}” -format UDZO -imagekey zlib-level=9 -o “${OUTPUT_DMG}” ${HDIUTIL_VERBOSITY}
# Sign DMG if needed. if [[ ! -z “${CODE_SIGN_IDENTITY}” ]]; then
echo "✍️ Codesign '${OUTPUT_DMG}'" codesign -fv -s "${CODE_SIGN_FINGERPRINT_ID}" "${OUTPUT_DMG}"
fi
# Verify DMG. echo “🧐 Verify ‘${OUTPUT_DMG}’” hdiutil verify “${OUTPUT_DMG}” ${HDIUTIL_VERBOSITY}
# Remove temp DMG. if [[ -f “${DMG_TEMP}” ]]; then
echo "🧹 Clean-up" rm -Rf "${DMG_TEMP}"
fi