После завершения разработки под OS X может остаться ощущение незавершенности — для полного счастья хотелось бы видеть свое приложение в каталоге, тем более, что это, пожалуй, лучшая площадка для продажи десктопных приложений. На эту тему есть статья времен Qt 4.8 в официальном блоге, и еще более старая на хабре. К счастью, больше нет необходимости пересобирать Qt, однако с приходом OS X 10.9 некоторые баги стали критичными, приходится выкручиваться.
Не буду описывать тривиальные и давно уже разобранные вещи вроде получения статуса разработчика, создания provision profile, регистрации нового приложения в iTunes Connect. Будем считать, что все это уже настроено, программа готова, и только ждет своего часа. Мне не хотелось задействовать XCode, потому из дополнительного ПО нам потребуется Application Loader, который можно загрузить так. В качестве примеров приведены файлы нашего органайзера для студентов iStodo, так получается менее абстрактно и более приближено к реальности.
Итак, для начала нужны три дополнительных файла: иконка*, Info.plist, Entitlements.plist
Info.plist
Этот файл содержит всю информацию о приложении (таблица рекомендуемых полей тут).
Вот минимальный каркас:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>Russian</string>
<key>CFBundleDisplayName</key>
<string>iStodo</string>
<key>CFBundleExecutable</key>
<string>iStodo</string>
<key>CFBundleIconFile</key>
<string>iStodo.icns</string>
<key>CFBundleIdentifier</key>
<string>ru.istodo.istodo</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>iStodo</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.productivity</string>
</dict>
</plist>
Для того, чтобы нестандартный plist автоматически добавлялся в бандл при каждой сборке, нужно добавить в файл проекта:
QMAKE_INFO_PLIST= $${PWD}/Info.plist
Заодно сразу добавим в .pro файл пару строк для отладки:
QMAKE_CFLAGS += -gdwarf-2
QMAKE_CXXFLAGS += -gdwarf-2
Entitlements.plist
Программы, устанавливаемые через App Store, будут работать в песочнице, для этого необходимо произвести некоторые приготовления. По правилам, нам, через QDesktopServices::storageLocation(), доступна папка вида имя_компании/имя_приложения (как это указано в iTunes Connect), потому нужно задать эти параметры явно:
QApplication::setOrganizationName("MyCompany")
QApplication::setApplicationName("MyApp")
Entitlements же, по сути, — файл разрешений, и те функции, которые в нем не указаны, будут заблокированы. Например, если мы хотим делать что-нибудь с сетью, следует установить флаг com.apple.security.network.client, и т.д. Возможные ключи с описаниями тут.
Ну и пример готового файла:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>
Публикация
Итак, можно выделить следующие ключевые моменты:
- Скопировать фреймворки и плагины Qt внутрь папки приложения
- Подписать
- Упаковать в формат .pkg
- Залить в iTunes Connect
Скопировать
Для того, чтобы программа могла запускаться не только на системах с установленным Qt SDK, нужно запустить специализированную утилиту — macdeployqt, которая скопирует все необходимые плагины и фреймворки в бандл приложения. К сожалению, из-за ошибки в этой утилите не копируются файлы Info.plist для фреймворков, что не является проблемой для работы, но без них не получится корректно подписать приложение. Нужно заметить, что если программа использует модуль QtSql, будут скопированы все доступные драйверы. Все бы ничего, но из-за libqsqlodbc.dylib приложение отклоняют с формулировкой «За использование приватных методов», а из-за libqsqlpsql.dylib ругаются на использование устаревшей библиотеки. Чтобы не искушать судьбу, перед публикацией стоит снести ненужные драйверы, заодно немного уменьшить размер пакета. Также для уменьшения размера можно удалить лишние плагины форматов изображений, и т.д.
Подписать
Начиная с OS X 10.9, нужно обязательно подписывать не только приложение, но и все фреймворки, плагины. Не смотря на то, что в Technical Note 2206 написано, что правильным будет подписывать не папку фреймворка, а каталог версии, на практике сделать это не получается, да и нареканий при ревью не возникло.
Примеры команд:
codesign -s "3rd Party Mac Developer Application: Developer Name" myApp.app/Contents/Frameworks/QtSql.framework/
codesign -s "3rd Party Mac Developer Application: Developer Name" myApp.app/Contents/PlugIns/platforms/libqcocoa.dylib
После подписывания всех библиотек приходит очередь приложения целиком, как раз тут и используется файл Entitlements:
codesign --entitlements myAppEntitlements.plist -s "3rd Party Mac Developer Application: Developer Name" myApp.app
Проверяем, все ли прошло гладко:
codesign --display --verbose=4 myApp.app
Упаковать
Тут все делается одной командой, которая, впрочем, изменилась с момента написания официального мануала:
productbuild --component "myApp.app" /Applications --sign "3rd Party Mac Developer Installer: Developer Name" --product "myApp.app/Contents/Info.plist" myApp.pkg
Для пробы можно сразу же запустить получившийся пакет:
sudo installer -store -pkg myApp.pkg -target /
Залить в iTunes Connect
Опять же ничего сложного, выбираем пункт Deliver your app, свое приложение, указываем путь до .pkg. Многие сталкиваются с проблемой (зависанием) в процессе заливки, рабочее решение тут.
Скрипт
В итоге получается очень много ручного труда: копировать файлы .plist для фреймворков, подписывать каждую библиотеку… Был написан скрипт на python, который полностью автоматизирует процесс. Предполагается, что скрипт лежит в каталоге сборки (или, по крайней мере, в одном каталоге с бандлом программы). Будет создана отдельная папка вида myApp_1.2, в которой, если все пройдет как надо, появится результат работы — файл .pkg. Для настройки следует отредактировать блок с параметрами — указать название приложения, версию, расположение Qt, название файла с разрешениями, информацию о разработчике:
version = "1.2"
appName = "myApp"
devName = "Developer Name"
pathToQt = "/Users/_USER_NAME_/Qt5.2.0/5.2.0/clang_64/"
entitlements = "myAppEntitlements.plist"
# -*- coding: utf-8 -*-
import os
import glob
import shutil
from subprocess import call
# Setup app info (Don't forget to change the version in the Info.plist)
version = "1.2"
appName = "myApp"
devName = "Developer Name"
pathToQt = "/Users/_USER_/Qt5.2.0/5.2.0/clang_64/"
entitlements = "myAppEntitlements.plist"
fullApp = appName +".app"
dirName = appName + "_" + version
# if we need only libqsqlite.dylib
sqliteOnly = True
sqldriversDir = fullApp+"/Contents/PlugIns/sqldrivers/"
frameworksDir = fullApp+"/Contents/Frameworks/"
pluginsDir = fullApp+"/Contents/PlugIns/"
print("Prepearing to deploy...")
# Check files and paths
if not os.path.exists(pathToQt) or not os.path.isdir(pathToQt):
print("Incorrect path to Qt")
exit()
if not os.path.exists(fullApp) or not os.path.isdir(fullApp):
print("App bundle not found")
exit()
if not os.path.exists(entitlements) or os.path.isdir(entitlements):
print("Entitlements file not found")
exit()
#remove old build
if os.path.exists(dirName):
shutil.rmtree(dirName)
os.makedirs(dirName)
# Copy all necessary files to new folder
shutil.copy(entitlements, dirName)
shutil.copytree(fullApp, dirName+"/"+fullApp)
# Copy Qt libs for create independent app
os.chdir(os.getcwd()+"/"+dirName)
print("nDeploying Qt to .app bundle...")
call([pathToQt+"bin/macdeployqt", fullApp])
print("...donen")
# Other libs in Qt 5.2(at least) will be rejected from Mac App Store anyway
if sqliteOnly and os.path.exists(sqldriversDir):
sqllibs = glob.glob(sqldriversDir+"*.dylib")
for lib in sqllibs:
if os.path.basename(lib) != "libqsqlite.dylib":
os.remove(lib)
# Copy plists for frameworks (it's fix macdeployqt bug)
frameworks = os.listdir(frameworksDir)
for framework in frameworks:
shutil.copy(pathToQt+"lib/"+framework+"/Contents/Info.plist", frameworksDir+framework+"/Resources/")
print("nSigning frameworks, dylibs, and binary...")
# Sign frameworks (it's strange, but we can't sign "Versions" folder)
os.system('codesign -s "3rd Party Mac Developer Application: '+devName+'" '+frameworksDir+"*")
# Sign plugins
pluginGroups = os.listdir(pluginsDir)
for group in pluginGroups:
os.system('codesign -s "3rd Party Mac Developer Application: '+devName+'" '+pluginsDir+group+"/*")
# Sign app
os.system('codesign --entitlements '+entitlements+' -s "3rd Party Mac Developer Application: '+devName+'" '+fullApp)
print("nCheck signing:")
os.system("codesign --display --verbose=4 "+fullApp)
# - - -
print("nBuilding package...")
os.system('productbuild --component "'+fullApp+'" /Applications --sign "3rd Party Mac Developer Installer: '+devName+'" --product "'+fullApp+'/Contents/Info.plist" '+appName+'.pkg')
print("...donen")
print('nFor test install, run follow command: sudo installer -store -pkg '+dirName+'/'+appName+'.pkg -target /')
Результат — https://itunes.apple.com/ru/app/istodo/id840850188?mt=12
В заключение можно сказать, что подготовить Qt приложение к публикации в Mac App Store не особо сложно, но делать все вручную — муторно.
Официальные руководства:
developer.apple.com/library/mac/documentation/IDEs/Conceptual/AppDistributionGuide/Introduction/Introduction.html
developer.apple.com/library/mac/documentation/IDEs/Conceptual/AppDistributionGuide/SubmittingYourApp/SubmittingYourApp.html
Автор: Zifix