KengoSawa2の技術的ななにか

IT屋さんのようなKengoSawa2がなんかそれっぽい事を書いていくblogです

Mac App Storeで販売可能なアプリをビルドするqmakeの例と簡易解説

こんにちは、KengoSawa2です。
レスパスビジョン株式会社で社内向けツールをQtで作成したり、www.lespace.co.jp
開発を行っていたりする謎のエンジニアです。

今日は唐突ですが、上記RapidCopyのqmakeについて、解説適当に内容を晒してみようと思います。

何故qmakeを適当に晒すか?

何故唐突にqmakeを晒すのか?って話になるのですが、理由は単純です。

「Qtを使ってMac AppStore用アプリを作るためのqmakeの参考資料」がゼロだったから

です。
あまりにも情報が無さ過ぎて非常に苦労したので、後続の方がこの記事を見て参考にして楽してくれればなと思い、書いてみました。

早速のqmake

あれこれ解説するよりも、qmakeをそのまま貼った方が良いでしょうということでとりあえず貼りましょう。
例によってですが、一つ一つのコマンドが何をしてるかをなんとなく書いているので、中身の詳細は割愛します。

QT += gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets multimedia

#CONFIGパラメータ指定の意味は以下の通りね
#何も指定しない(コメントアウト) = Mac AppStore用コンパイル(Sandbox対応版)
#pro                       = Pro版をビルド(レスパス直販版かつ、sandbox非対応)
#trial                     = 体験版をビルド(ビルド時刻から2ヶ月経過すると起動ブロック)
#amazon                    = amazon.com販売版をビルド
#上記を組み合わせて使用する

#以下の場合はRapidCopyProを体験版としてビルド指定
CONFIG += pro trial

if(pro){
	#pro版の場合は.appの名称をRapidCopyProとする
	TARGET = RapidCopyPro
	#amazon.com向けの場合はifdefに_AMAZONを指定
	if(amazon){
		DEFINES += _AMAZON
	}
}
else{
	#pro指定がないのでAppStore向け
	#.appの名称をRapidCopyとする
	TARGET = RapidCopy
	#AppStore向けの場合はifdefに_SANDBOXを指定
	DEFINES += _SANDBOX
}

if(trial){
	#体験版の場合はifdefに_TRIALを指定
	DEFINES += _TRIAL
}

#OS Xの開発環境ではQtがコンパイルされた時の環境より新しいバージョンのXCodeがリリースされると
#Appkitのヘッダなどのインクルード先が変更されるため、コンパイル不能になる。
#この時、以下のように最新のインクルード先を指定する必要がある。
#RapidCopyはQt5.4.1を使用するため、10.10までは以下を切る必要なかったのよ。。
QMAKE_MAC_SDK = macosx10.11

#ライブラリ開発じゃなくて、単体アプリケーション作るのでapp指定
TEMPLATE = app

#RapidCopyとRapidCopyProとでアイコンを変える
if(pro){
	ICON = RapidCopyPro.icns
}
else{
	ICON = RapidCopy.icns
}

#日本語翻訳ファイルを指定
TRANSLATIONS += RapidCopy_ja_JP.ts

#-LオプションにOS Xアプリのライブラリをリンクする指定、アプリならまず必須。
LIBS += -framework AppKit

#各種ソース(C,C++)
SOURCES += main.cpp \
	mainwindow.cpp \
	cfg.cpp \
	fastcopy.cpp \
	osl.cpp \
	regexp.cpp \
	tapi32ex.cpp \
	tapi32u8.cpp \
	tapi32v.cpp \
	tlist.cpp \
	tmisc.cpp \
	utility.cpp \
	version.cpp \
	mainsettingdialog.cpp \
	confirmdialog.cpp \
	aboutdialog.cpp \
	xxhash.c \
	finactdialog.cpp \
	smtp.cpp \
	jobdialog.cpp \
	registerdialog.cpp \
	qblowfish.cpp \
	joblistrenamedialog.cpp

#各種ヘッダ(C,C++)
HEADERS += \
	cfg.h \
	fastcopy.h \
	osl.h \
	regexp.h \
	tapi32ex.h \
	tapi32u8.h \
	tapi32v.h \
	tlib.h \
	tmisc.h \
	utility.h \
	version.h \
	resource.h \
	miscdlg.h \
	mainwindow.h \
	mainsettingdialog.h \
	confirmdialog.h \
	aboutdialog.h \
	xxhash.h \
	finactdialog.h \
	smtp.h \
	jobdialog.h \
	cocoaapi.h \
	registerdialog.h \
	qblowfish.h \
	qblowfish_p.h \
	joblistrenamedialog.h

#Objective-cソース
OBJECTIVE_SOURCES += \
	GetCocoaOpenFile.mm

#Objective-cヘッダ
OBJECTIVE_HEADERS += cocoaapi.h

#各種.uiファイル
FORMS	+= mainwindow.ui \
	mainsettingdialog.ui \
	confirmdialog.ui \
	aboutdialog.ui \
	finactdialog.ui \
	jobdialog.ui \
	registerdialog.ui \
	joblistrenamedialog.ui

#Qtの各種リソースファイル
RESOURCES += res.qrc


macx {
	#Mac DevelopperプログラムでAppleに発行して貰うアプリケーション開発元証明書情報文字列
	APPCERT = "3rd Party Mac Developer Application: L'ESPACE VISION CO.,LTD"

	#Mac DevelopperプログラムでAppleに発行して貰うインストーラ開発元証明書情報文字列
	INSTALLERCERT = "3rd Party Mac Developer Installer: L'ESPACE VISION CO.,LTD"
	
	#Mac Developper Programで申請、認可されたBundleIdentifierを指定
	if(pro){
		BUNDLEID = com.LespaceVision.RapidCopyPro
	}
	else{
		BUNDLEID = com.LespaceVision.RapidCopy
	}

	#Qt実行環境へのパス
	QTPATH = ~/Qt5.4/5.4/clang_64

	#ワークディレクトリ設定
	QTPROJECTS = /Users/lespace/QtProjects
	
	#アプリケーションのplistやアプリケーションに同時に格納するQtライブラリのplistを格納するフォルダのパスを設定
	#同ディレクトリ内にアプリに必要なInfo.plistを事前に配置する
	PLISTLOC = $${QTPROJECTS}/plist
	
	#RapidCopyPro専用。インストーラを作成するのに必要な各種リソースへのパスを定義する
	#RapidCopyPro.appからInfo.plistを生成、利用するためのワークディレクトリ
	PROPKGLOC = $${QTPROJECTS}/pkgpath
	#インストーラの各種設定を記述したxmlファイルを格納しているフォルダへのパス。事前に配置しておく。
	PRORESLOC = $${QTPROJECTS}/resource
	
	#pkgbuildコマンドに渡すバージョン番号、次verのリリース時は手で変更する
	PROVER = 1.1.0

	#Mac AppStoreに提出する場合の、SandBox制約例外を記述するplist,エンタイトルメントplistの指定。事前に配置しておく。
	ENTITLEMENTS = $${PLISTLOC}/Entitlementsplist/Entitlements.plist

	#以下,make codesignまたはmake productを実行した時行う作業に関する記述
	QMAKE_EXTRA_TARGETS += codesign product
	#以下の文法を使用してcommandsに+=してから最後にQMAKE_EXTRA_TARGETSにcodesignを追加すると、make codesignでcodesignに追加したcommandsを
	#順番に実行してくれる。
	
	#RapidCopy(Pro).app内にQt実行用ライブラリをmacdeployqtコマンド
	codesign.commands += $${QTPATH}/bin/macdeployqt $${TARGET}.app -verbose=3;

	#事前に記述、用意しておいたRapidCopy(pro)のInfo.plistを最終バイナリ内のInfo.plistに上書きする
	if(pro){
		codesign.commands += cp $${PLISTLOC}/Infoplist_pro/Info.plist $${TARGET}.app/Contents/Info.plist;
	}
	else{
		codesign.commands += cp $${PLISTLOC}/Infoplist/Info.plist $${TARGET}.app/Contents/Info.plist;
	}

	#macdeployqtでコピーした最終バイナリ中にあるQtライブラリのフレームワーク一つ一つをMac Developper Programで受け取った証明書でサインする。
	#(これをやらないとMac AppStoreに提出できない)
	codesign.commands += codesign -f -s \"$${APPCERT}\" $${TARGET}.app/Contents/Frameworks/QtCore.framework;
	codesign.commands += codesign -f -s \"$${APPCERT}\" $${TARGET}.app/Contents/Frameworks/QtMultimedia.framework;
	codesign.commands += codesign -f -s \"$${APPCERT}\" $${TARGET}.app/Contents/Frameworks/QtNetwork.framework;
	codesign.commands += codesign -f -s \"$${APPCERT}\" $${TARGET}.app/Contents/Frameworks/QtPrintSupport.framework;
	codesign.commands += codesign -f -s \"$${APPCERT}\" $${TARGET}.app/Contents/Frameworks/QtGui.framework;
	codesign.commands += codesign -f -s \"$${APPCERT}\" $${TARGET}.app/Contents/Frameworks/QtMultimediaWidgets.framework;
	codesign.commands += codesign -f -s \"$${APPCERT}\" $${TARGET}.app/Contents/Frameworks/QtOpenGL.framework;
	codesign.commands += codesign -f -s \"$${APPCERT}\" $${TARGET}.app/Contents/Frameworks/QtWidgets.framework;

	#フレームワークへのサインと同様、プラグインのライブラリにも全てサインする
	if(pro){
		codesign.commands += codesign -f -s \"$${APPCERT}\" $${TARGET}.app/Contents/PlugIns/audio/*.dylib;
		codesign.commands += codesign -f -s \"$${APPCERT}\" $${TARGET}.app/Contents/PlugIns/bearer/*.dylib;
		codesign.commands += codesign -f -s \"$${APPCERT}\" $${TARGET}.app/Contents/PlugIns/imageformats/*.dylib;
		codesign.commands += codesign -f -s \"$${APPCERT}\" $${TARGET}.app/Contents/PlugIns/mediaservice/*.dylib;
		codesign.commands += codesign -f -s \"$${APPCERT}\" $${TARGET}.app/Contents/PlugIns/platforms/*.dylib;
		codesign.commands += codesign -f -s \"$${APPCERT}\" $${TARGET}.app/Contents/PlugIns/printsupport/*.dylib;
	}
	else{
		codesign.commands += codesign -f -s \"$${APPCERT}\" -i $${BUNDLEID} $${TARGET}.app/Contents/PlugIns/audio/*.dylib;
		codesign.commands += codesign -f -s \"$${APPCERT}\" -i $${BUNDLEID} $${TARGET}.app/Contents/PlugIns/bearer/*.dylib;
		codesign.commands += codesign -f -s \"$${APPCERT}\" -i $${BUNDLEID} $${TARGET}.app/Contents/PlugIns/imageformats/*.dylib;
		codesign.commands += codesign -f -s \"$${APPCERT}\" -i $${BUNDLEID} $${TARGET}.app/Contents/PlugIns/mediaservice/*.dylib;
		codesign.commands += codesign -f -s \"$${APPCERT}\" -i $${BUNDLEID} $${TARGET}.app/Contents/PlugIns/platforms/*.dylib;
		codesign.commands += codesign -f -s \"$${APPCERT}\" -i $${BUNDLEID} $${TARGET}.app/Contents/PlugIns/printsupport/*.dylib;
	}

	#バンドル内のフレームワーク及びプラグイン全てにコードサインが終わったらバンドルである.appをコードサインする
	if(pro){
		codesign.commands += codesign -f -s \"$${APPCERT}\" -v $${TARGET}.app;
	}
	else{
		#Mac AppStoreに提出する場合にはここでEntitlementsのplistを指定しておく必要がある。
		codesign.commands += codesign -f -s \"$${APPCERT}\" -v --entitlements $${ENTITLEMENTS} $${TARGET}.app;
	}

	#ここからproductbuildコマンドを使用した最終ビルドの組み立て処理
	if(pro){
		#pro版の場合は、ようこそ的なインストーラ画面の設定や旧verが既に存在していた場合の対応などを手動でルール決めするのでやや複雑。

		#codesignまで終わったバンドルを一時pkgディレクトリに複製
		product.commands += cp -rf $${TARGET}.app $${PROPKGLOC};
		#一時pkgディレクトリ内のバンドルからproductbuildに必要なplistを逆生成する
		product.commands += pkgbuild --analyze --root $${PROPKGLOC} $${PROPKGLOC}/$${TARGET}.plist;
		#逆生成したplist内にplutilコマンドを使って、上書きインストール時の挙動設定を行う。
		#BundleIsRelocatableをfalseに設定しないと、既に古いverがインストール済みの場合に正しくインストールしてくれない
		product.commands += plutil -replace BundleIsRelocatable -bool false $${PROPKGLOC}/$${TARGET}.plist;
		#BundleIsVersionCheckedをfalseに設定しないと、あえて古いverを上書きインストールしたい場合に戻れなくなる
		product.commands += plutil -replace BundleIsVersionChecked -bool false $${PROPKGLOC}/$${TARGET}.plist;
		#いじくったplistを元に、インストール用のサブコンポーネントを作成する。
		#補足:RapidCopy(Pro)ではサブコンポーネントが1個しかないので、下のコマンドが一個だけど複数のパッケージを一つのインストーラに合体させたりもできるらしい。
		product.commands += pkgbuild --root $${PROPKGLOC} --component-plist $${PROPKGLOC}/$${TARGET}.plist --identifier $${BUNDLEID} --version $${PROVER} --install-location /Applications $${TARGET}.pkg;
		#最終インストーラをproductbuildコマンドを用いて作成する。
		product.commands += productbuild --distribution $${PRORESLOC}/Dist.xml --package-path $${PROPKGLOC} --resources $${PRORESLOC} $${TARGET}Installer.pkg;
		#一時pkgディレクトリ内の各種一時ファイルを削除
		product.commands += rm -rf $${PROPKGLOC}/*;
	}
	else{
		#AppStore版の場合はインストーラ部分が不要、かつバージョン管理などは全てAppStoreの仕掛けにお任せなので、以下だけでok
		product.commands += productbuild --component $${TARGET}.app /Applications --sign \"$${INSTALLERCERT}\" $${TARGET}.pkg;
	}
}

実際に製品ビルドするときは

僕の場合は以下のような適当なシェルスクリプトを用意して、製品ビルドするときだけシェルをターミナルから叩いています。

!/bin/bash

cd /Users/lespace/QtProjects/build-rapidcopy_main-Desktop_Qt_5_4_1_clang_64bit-Release
rm -rf RapidCopy*.app
make clean
make
make codesign
make product

内容はなんというか、だいぶ適当ですがポイントは

make codesign
make product

のところですね。
この引数はQMAKE_EXTRA_TARGETSで指定した任意文字列なわけです。

QtCreatorにはmakeに引数を渡して実行する機能もあるので、それを活用するともっと便利かもしれませんが、
ターミナルにばらばらーっと出てくれる方が好みなので、とりあえずはこれでいいやー感です。
大規模開発の方などはもっと色々詰めないといけないでしょうね。

今回の記事を書くにあたって、無駄なところを色々消したりして綺麗にしてみましたが、
もっと頭の良いやり方はいっぱいあると思います。。
OSXのqmakeに詳しい方のツッコミお待ちしております!!

また、Mac環境でQtを使って開発をしているかた、twitterKengo Sawatsu (@KengoSawa2) | Twitter
でも、Qt勉強会でもなんでも良いので、是非情報交換しましょうー。

明日(12/2)はQt勉強会のおやつ部部長緑の翁 (@hermit4) | Twitterさんの記事です!!