Приветствую достопочтенных жителей !
Сегодня я расскажу вам о том, как красиво преподнести пользователю инсталлятор своей программы. Наверняка каждый, кто пользуется не только программами из AppStore, сталкивался с красивыми образами диска .dmg, как вот у Адиума, к примеру. Такой образ представляет из себя, так сказать, интерактивный инсталлятор, в котором дана чёткая подсказка: перетащи значок вот сюда. Всё предельно понятно и просто.
Конечно, для бывалого маковода и zip-архив сгодится, но ведь все любят, чтоб было красиво и удобно. Так что мы с вами, дорогие читатели, озаботимся сегодня созданием такого вот красивого образа диска для своей (ну или чужой) программы.
Можно, конечно, проделать это всё вручную, но это не_наш_метод™, так что мы будем писать shell-скрипт для автоматизации сего процесса. Автоматизация нам так же пригодится в случае ввода в эксплуатацию билд-сервера, тогда этот сервер будет не только собирать программу из исходников, но и делать образ диска для дистрибьюции.
Если Вам не нужно знать как работает такой скрипт, а нужен лишь инструмент — в конце имеется ссылка на весьма универсальный скрипт, готовый к применению.
План действий вкратце:
- Скопировать бандл во временную директорию
- Создать образ диска из временной директории, доступный для чтения и записи
- Примонтровать полученный образ
- Кастомизировать внешний вид образа с помощью AppleScript: создать симлинк для /Applications, задать фон и расположение элементов
- Задать иконку для образа диска
- Отмонтировать образ
- Конвертировать образ в сжатый ридонли
- Готово! Можно выкладывать на сайт или в Sparkle-каст.
ВАЖНО! Скрипт (точнее, его часть, задействующая AppleScript) не будет работать должным образом, если установлен TotalFinder (или аналогичная штука), и это может привести к непредсказуемым последствиям! Серьёзно.
Теперь по порядку по всем пунктам. Ссылка на готовый скрипт в конце статьи.
Для начала нам надо определить что будем упаковывать и какое имя образа получим в конце. Для этого в скрипте пишем так:
TMP_DIR="./tmp"
APP_BUNDLE_NAME="MyGreatApplication.app"
APP_VERSION=`/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "${APP_BUNDLE_NAME}/Contents/Info.plist"`
APP_BUILD_VERSION=`/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "${APP_BUNDLE_NAME}/Contents/Info.plist"`
DMG_NAME_BASE=${APP_BUNDLE_NAME%.*}
DMG_NAME_SUFFIX=" ${APP_VERSION}.${APP_BUILD_VERSION}"
DMG_NAME="${DMG_NAME_BASE}${DMG_NAME_SUFFIX}.dmg"
VOL_NAME=${APP_BUNDLE_NAME%.*}
Здесь мы задаём имя бандла, который будем паковать, и читаем из него версию и билд программы с помощью утилиты PlistBuddy и устанавливаем их как суффикс для имени файла будущего образа диска. Имя же образа при монтировании мы задаём просто как имя бандла без расширения.
Для создания временного образа диска используем программу hdiutil, вызвав её со следующими параметрами
hdiutil create -ov -srcfolder ${TMP_DIR} -format UDRW -volname "${VOL_NAME}" "${DMG_NAME_TMP}"
Здесь параметры TMP_DIR
, VOL_NAME
и DMG_NAME_TMP
— временная директория, имя образа (которое будет отображаться при монтировании) и имя временного .dmg файла соответственно. Параметр -format UDRW
указывает на тип образа: UDIF образ для чтения и записи. Возможность записи на диск нам нужна для кастомизации внешнего вида примонтированного образа. Параметр -ov указывает утилите перезаписать образ, если вдруг мы его не удалили в прошлый раз.
Теперь монтируем полученный образ (и сохраняем имя устройства для последующего использования):
device=$(hdiutil attach -readwrite -noverify -noautoopen ${DMG_NAME_TMP} | egrep '^/dev/' | sed 1q | awk '{print $1}')
Здесь всё довольно просто: аттачим с правами на чтение и запись, автоматически окно файндера не открываем, а затем получаем имя типа /dev/disk2
с помощью egrep, sed и awk парся вывод hdiutil'а.
Теперь скопируем на образ фоновую картинку и иконку, которые лежат, к примеру, в ресурсах нашего бандла (хотя, конечно, их можно взять и из любого другого места):
BG_FOLDER="/Volumes/${VOL_NAME}/.background"
mkdir "${BG_FOLDER}"
cp "${APP_BUNDLE_NAME}/Contents/Resources/${BG_IMG_NAME}" "${BG_FOLDER}/"
ICON_FOLDER="/Volumes/${VOL_NAME}"
cp "${APP_BUNDLE_NAME}/Contents/Resources/${VOL_ICON_NAME}" "${ICON_FOLDER}/.VolumeIcon.icns"
Далее, нам надо подмонтированный образ кастомизировать, сделать это можно через Finder вручную, но мы поступим хитрее: заставим Finder сделать всё автоматически через AppleScript. Для вызова таких скриптов из шелла есть утилита osascript, которой на вход подадим такую заготовку:
APPLESCRIPT="
tell application "Finder"
tell disk "${VOL_NAME}"
open
-- Setting view options
set current view of container window to icon view
set toolbar visible of container window to false
set statusbar visible of container window to false
set the bounds of container window to {${WINDOW_LEFT}, ${WINDOW_TOP}, ${WINDOW_RIGHT}, ${WINDOW_BOTTOM}}
set theViewOptions to the icon view options of container window
set arrangement of theViewOptions to not arranged
set icon size of theViewOptions to 72
-- Settings background
set background picture of theViewOptions to file ".background:${BG_IMG_NAME}"
-- Adding symlink to /Applications
make new alias file at container window to POSIX file "/Applications" with properties {name:"Applications"}
-- Reopening
close
open
-- Rearranging
set the position of item "Applications" to {${APPS_X}, ${APPS_Y}}
set the position of item "${APP_BUNDLE_NAME}" to {${BUNDLE_X}, ${BUNDLE_Y}}
-- Updating and sleeping for 5 secs
update without registering applications
delay 5
end tell
end tell
"
echo "$APPLESCRIPT" | osascript
Разумеется, вместо VOL_NAME
и прочего наш shell-скрипт подставит заранее заготовленные строки. Данный AppleScript говорит файндеру открыть наш подмонтированный диск, задать ему нужные параметры отображения: убрать строку адреса и статуса, задать вид «иконки», задать размеры окна, размер иконок. Все эти параметры будут сохранены в файле .DS_Store. Далее мы задаём фоновую картинку, скопированную ранее. Затем — создаём симлинк (алиас в терминах AS) для /Applications, переоткрываем окно для применения изменений. Теперь устанавливаем положение иконок программы и созданного симлинка, обновляем и спим 5 секунд для надёжности.
Теперь установим иконку для образа через утилиту SetFile:
SetFile -c icnC "${ICON_FOLDER}/.VolumeIcon.icns"
SetFile -a C "${ICON_FOLDER}"
Далее, устанавливаем нужные права для образа диска, синхронизируемся (два раза для надёжности) и извлекаем образ:
chmod -Rf go-w /Volumes/"${VOL_NAME}"
sync
sync
hdiutil detach ${device}
Всё, теперь можно делать финальный образ диска!
hdiutil convert "${DMG_NAME_TMP}" -format UDZO -imagekey zlib-level=9 -o "${DMG_NAME}"
Здесь мы с помощью hdiutil конвертируем временный образ в формат UDZO (UDIF сжатый) с уровнем компрессии 9 (лучшая). И на выходе получаем образ диска для дистрибьюции, имеющий привлекательный внешний вид!
Полный скрипт включён в репозиторий на гитхабе. Замечу, что в полной версии скрипта очень много возможных входных параметров (парсинг которых сильно увеличивает его размер), хардкода нет совсем (ну кроме дефолтных параметров). Ну а картинка, приведённая в начале поста, получается следующим вызовом моего скрипта:
$ make_dmg.sh -V -b habr_logo_big.png -i habr_icon.icns -s "800:500" -c 535:345:253:345 "Hello Habr.app"
Enabling version info in resulting dmg
Setting background to habr_logo_big.png
Setting icon to habr_icon.icns
Setting window size to 800:500
Setting coordinates to 535:345:253:345
Bundle name set to Hello Habr.app
Defaulting dmg volume name to Hello Habr
*** Copying Hello Habr.app to the temporary dir... done!
*** Creating temporary dmg disk image.........
created: /Users/silvansky/Projects/habr_demo_dmg/Hello Habr_tmp.dmg
*** Mounting temporary image... done! (device /dev/disk1)
*** Sleeping for 5 seconds... done!
*** Setting style for temporary dmg image...
* Copying background image... done!
* Copying volume icon... done!
* Setting volume icon... done!
* Executing applescript for further customization... done!
*** Converting tempoprary dmg image in compressed readonly final image...
* Changing mode and syncing...
chmod: /Volumes/Hello Habr/.Trashes: Permission denied
* Detaching /dev/disk1...
"disk1" unmounted.
"disk1" ejected.
* Converting...
Готовлюсь к созданию образа…
Читаю Driver Descriptor Map (DDM : 0)…
(CRC32 $AF5ACFAD: Driver Descriptor Map (DDM : 0))
Читаю Apple (Apple_partition_map : 1)…
(CRC32 $92261EDC: Apple (Apple_partition_map : 1))
Читаю disk image (Apple_HFS : 2)…
...................................................................................................................................................................................
(CRC32 $F59F12B2: disk image (Apple_HFS : 2))
Читаю (Apple_Free : 3)…
....................................................................................................................................................................................
(CRC32 $00000000: (Apple_Free : 3))
Добавляю ресурсы…
....................................................................................................................................................................................
Прошло: 804.502ms
Размер файла: 4132028 байт, сумма: CRC32 $AFE83FC5
Обработано секторов: 13243, 10041 сжато
Скорость: 6.1 Mб/с
Сохранений: 39.1 %
created: /Users/silvansky/Projects/habr_demo_dmg/Hello Habr 1.0.1.dmg
done!
*** Removing temporary image... done!
*** Cleaning up temp folder... done!
*** Everything done. DMG disk image is ready for distribution.
Что ж, теперь Вы можете написать свой скрипт для таких целей или же использовать мой, достаточно универсальный. Успешных проектов!
Автор: silvansky
Спасибо, полезный скриптик.