働きたくないでござる!
絶対に IDE で働きたくないでござる!!
……というわけで、 mac 用のアプリも CUI ベースで開発してビルドも make で行いたいというような話です。
マルチプラットフォームでビルドしたいので cmake を使います。
mac のバイナリ(?)である bundle は、要するに何の変哲もないディレクトリで中身にお作法があります。
ここにはアプリケーションの実行形式とアプリケーションの MANIFEST である InfoList.plist 、そしてメインウインドウを記述する .nib ファイルが最低限必要です。
それ以外にも、必要に応じて so や、リソースファイルなどが必要になるでしょう。
こうしたものは Xcode を使えば特に気にすることもなく出来てしまうのですが、そもそも Xcode 使いたくないじゃん?というようなところに動機があります。
Linux や cygwin にも対応できるよう、 gnu make をベースにビルドすることを考えます。
makefile を最初から書く覚悟のある人は、バンドルの規則に従ってターゲット書けばいいだけの話ですのでそうしてもらうとして、ここでは cmake を使って makefile や好みにあわせて .xproj なども作れるようにしておきましょう。
CMakeLists.txt を書く
framework の追加
まずは framework を追加しましょう。ぶっちゃけ GUI でチマチマ追加するより簡単ですよ。
cmake_minimum_required(VERSION 2.8)
project(cocoa.app)
if (APPLE)
add_executable(cocoa MACOSX_BUNDLE main.mm app.mm)
include_directories ( /System/Library )
find_library(COCOA_LIBRARY Cocoa)
find_library(APPKIT_LIBRARY AppKit)
find_library(APP_SERVICES_LIBRARY ApplicationServices )
mark_as_advanced (COCOA_LIBRARY
APPKIT_LIBRARY
APP_SERVICES_LIBRARY)
set(EXTRA_LIBS ${COCOA_LIBRARY} ${APPKIT_LIBRARY} ${APP_SERVICES_LIBRARY})
target_link_libraries(cocoa ${EXTRA_LIBS})
endif (APPLE)
小文字は cmake の組み込み関数やディレクティブです。
この場合は project として cocoa.app というバンドル名を指定します。この CMakeLists.txt のルートですので、この場合はバンドル名とするのが自然と思います。さて、これは実行形式の名前とは無関係です。
実行形式は通常 project 名とは無関係で、任意の名前です。(一つのバンドルに複数の実行形式があってかまわないので)
実行形式は、ターゲットとして、 add_executable() で宣言されます。
これは極めて重要な指定で、これを行った以降でないと設定できない属性が沢山あります。実際は依存関係を記述する都合上、後方に書かれることが多いですが、ここでは解りやすさのためには真っ先に書いています。
ここでは特に、
MACOSX_BUNDLE としていることに注意してください。
mac 用の環境を切り離すのには if (APPLE) を使います。
find_library() で framework を探し、 mark_as_advanced() に設定します。
そのリストを target_link_libraries() で(上で宣言した)実行形式にリンクしましょう。
find_library() で指定するのは /System/Library/Frameworks/ 以下にある *.framework のワイルドカードにマッチする名前です。
さてここまでは簡単です。次にバンドルに突っ込むリソース類のレシピを記述します。
InfoList.plist の追加
バンドルに絶対欠かせないものの一つに InfoList.plist があります。こいつはいわゆる MANIFEST です。
バンドルは shell でさっくり実行形式をロードするのとは異なり、 Mac のシステムサービスを経由して実行されるので、これの記述は重要です。
絶対に欠かせないものは
- 実行形式の場所
- nib の名前
- NSPrincipal クラスの名前
です。これがないとバンドルはシステムのロンチサービスに蹴られてエラーになります。
他にも固有識別子やバンドル自体の名前など、重要そうなものがあります。
これらは基本的には cmake 固有の変数に値を設定することで勝手に InfoList.plist が作られるのですが、なぜか nib と NSPrincipal クラスの名前だけ、 cmake 固有の変数がありません。
従ってそのままだとバンドルができても、ロンチサービスに蹴られてしまいます。ありがちですね。
すぐ思いつく対策は、あらかじめ作り込んだ InfoList.plist を用意しておいてビルドのポストコマンドでコピーすることですが、それだとカスタマイズ性がよくありません。
そこでカスタム用の変数を宣言しましょう。カスタム変数は要するに cmake の置換文字列機能です。この場合は、 MACOSX_BUNDLE_INFO_PLIST
プロパティを使って、ベースとなる InfoList.plist のテンプレートを指定し、 その中身を任意の変数で置換すればよいわけです。
set_target_properties(cocoa PROPERTIES MACOSX_BUNDLE_INFO_PLIST Info-CMake.plist)
まず一点。
プロパティは変数とは違います。変数がプロセッシングの過程で置換されるだけのものだとしたら、プロパティは cmake の振る舞いを変えることができます。
cmake --help-property-list で一覧を見て、 cmake --help-property でその意味を調べられます。 MACOSX で grep すると以下のようになりました。
MACOSX_BUNDLE
MACOSX_BUNDLE_INFO_PLIST
MACOSX_FRAMEWORK_INFO_PLIST
MACOSX_PACKAGE_LOCATION
困ったときに役立つと思いますので、頭に入れておきましょう。
もう一つ気をつけなければならないのは、 自分のテンプレートを指定したらもう cmake 固有のテンプレートはもう使われないというところです。
なのでどこから適当に拾ってきた InfoList.plist からテンプレートを作ると、ここで使わなかった置換変数は使われなくなります。これではカスタマイズ性が下がります。
ここでは cmake が持っているテンプレートを参考にしたほうがよいでしょう。
cmake のテンプレートは、
/Applications/CMake\ 2.8-12.app/Contents//share/cmake-2.8/Templates/AppleInfo.plist
などにあると思います。
これを使うと以下のような plist が出来上がります。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>${MACOSX_BUNDLE_EXECUTABLE}</string>
<key>CFBundleGetInfoString</key>
<string>${MACOSX_BUNDLE_INFO_STRING}</string>
<key>CFBundleIconFile</key>
<string>${MACOSX_BUNDLE_ICON}</string>
<key>CFBundleIdentifier</key>
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLongVersionString</key>
<string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string>
<key>CFBundleName</key>
<string>${MACOSX_BUNDLE_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
<key>CSResourcesFileMapped</key>
<true/>
<key>LSRequiresCarbon</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
<key>NSMainNibFile</key>
<string>${MACOSX_BUNDLE_NSMAIN_NIB_FILE}</string>
<key>NSPrincipalClass</key>
<string>${MACOSX_BUNDLE_NSPRINCIPAL_CLASS}</string>
</dict>
</plist>
せっかく DTD がありますので、必須の要素がどれかなどは DTD か Apple のドキュメントを参照してください。
しかしながら、一点問題があります。
CFBundleExecutable の値を、デフォルトの cmake のテンプレートはターゲット名から自動で埋めてくれるのですが、カスタムテンプレートを使うとどうもその機能が動かなくなってしまいます。従って、 MACOS_BUNDLE_EXECUTABLE は明示的に指定するようにしましょう。(なくっても、バンドル名が 実行形式名.app なら勝手にそれを実行してくれるようですが)
set(MACOSX_BUNDLE_EXECUTABLE "cocoa")
set(MACOSX_BUNDLE_INFO_STRING "${PROJECT_NAME}")
set(MACOSX_BUNDLE_GUI_IDENTIFIER "com.example")
set(MACOSX_BUNDLE_LONG_VERSION_STRING "${PROJECT_NAME} Version ${VERSION}")
set(MACOSX_BUNDLE_BUNDLE_NAME ${PROJECT_NAME})
set(MACOSX_BUNDLE_SHORT_VERSION_STRING ${VERSION})
set(MACOSX_BUNDLE_BUNDLE_VERSION ${VERSION})
set(MACOSX_BUNDLE_COPYRIGHT "Copyright 2013.")
set(MACOSX_BUNDLE_NSMAIN_NIB_FILE "MainMenu")
set(MACOSX_BUNDLE_NSPRINCIPAL_CLASS "NSApplication")
set_target_properties(cocoa PROPERTIES MACOSX_BUNDLE_INFO_PLIST Info-CMake.plist)
まとめるとこんな感じでしょうか。
InfoList.plist については以上です。
.nib などのリソース類
最後にして大物です。
リソースは bundle.app/Contents/Resources 以下にある雑多なものですが、 .nib のようにウインドウシステムレベルで重要なものも含まれます。
雑多なリソースは依存を書いておけば勝手にコピーされますが、 nib に関してはそうはいきません。 .xib から .nib へのコンパイルなどはカスタムコマンドを定義してビルドのポストフェイズに走らせる必要があります。
これについては以下のようにするのがよいようです。
set(XIB MainMenu)
set_source_files_properties(
${MACOSX_RESOURCE_FILES}
# 任意のリソース群 (.nib は除く)
PROPERTIES
MACOSX_PACKAGE_LOCATION Resources)
find_program(IBTOOL ibtool HINTS "/usr/bin" "${OSX_DEVELOPER_ROOT}/usr/bin")
add_custom_command (TARGET cocoa PRE_BUILD
COMMAND mkdir -p ${PROJECT_NAME}/Contents/Resources)
add_custom_command (TARGET cocoa POST_BUILD
COMMAND ${IBTOOL} --errors --warnings --notices --output-format human-readable-text
--compile ${PROJECT_NAME}/Contents/Resources/${XIB}.nib
${XIB}.xib
COMMENT "Compiling ${XIB}.xib -> ${MODULE}/Contents/Resources/${XIB}.nib")
新たな変数が登場していますが、全部デフォルトでよいです。
基本は set_source_files_properties() でリソースファイルの MACOSX_PACKAGE_LOCATION を指定してやることと、 ibtool を探してビルドのポストフェイズに明示的にコンパイルするようにしてやることです。
.xib はこの場合一つですが、複数ある場合は foreach でリスト要素ごとに add_custom_command() でポストフェイズのコマンドを追加してやる必要があります。
パスを自分で指定しなおしてやったりと少々冗長なところもありますが汎用の関数だし、これでいいのです。
重要じゃありませんが、 find_program() も find_library() と同様よくつかう関数です。
さて、問題はどうやって .xib を用意するか、です。
XML エディタで作ってもいいですが、なんせ結構煩雑ですし、ドキュメントも DTD すらもありません。
ノードを削っていって最小限の .xib を作るというのも面白いところですが、結構でかいファイルですんでここは Xcode 様にご登場願いましょう。
一個作って使い回しましょう。直したいところだけ XML エディタで直しゃーいいじゃないですか。
ibtool は結構便利なプロセッサですが、真面目に UI を作ろうと思ったらどうしたって Interface Builder を使うことになりますから。
ではそういったところでお疲れさん。