Building C# iphone application using a Visual studio project file

はじめに

Visual stuid(以下,VS)oでMonotouchのライブラリを参照して作成したC# プロジェクトから,iPhoneアプリケーションをビルド,シミュレータで実行するまでの手順を紹介します.
小規模なチームでiPhoneアプリケーションを開発するときには,メンバーが最も慣れ親しんだソースコード開発環境をそのまま利用することは大きな価値があります.そこでVisual stuidoをソースコードおよびプロジェクト編集に用い,それらを元にiPhone アプリケーションをビルド,テストする環境を構築しました.
今の環境はシミュレータで実行することができます.ただしXIBファイルを使わない前提で作成しています.実際のデバイスでの実行は,私がまだその環境を持たないために,動作確認ができていません.

概要

前回のエントリで,VSのソースコード編集補完機能をMonotouchのdllを参照して有効にする方法を示しました.ここではVisual StudioのソリューションファイルからのMakefileの構築,およびMakeの実行を目標とします.

初期検討

開発環境をどうするかを検討しました.補完機能の便利さからVSもしくはMonoDevelop(以下,MD)いずれかのIDEを使います.Windowsではないプラットホームで開発することも考えると,いずれのIDEも使えることが望ましいです.これらの条件で,どちらのIDEを主に利用するかを検討しました.結果VSを主としました.
その検討過程は:

  • MDはiPhoneのプロジェクトファイルをサポートしていて,ビルド,テスト,インストールがIDEから実行できる.
  • MDで作成したiPhoneのプロジェクトファイルは,VSでは開けない (サポートされていないプロジェクトだとエラーが出る)
  • VSで"空のプロジェクト"で作成したプロジェクトファイルは,MDでも開いて編集できる.

これらからVSの"空のプロジェクト"で作成したソリューションを使うことにしました.

ビルドツールの検討

VSのソリューションからのビルド,テストの自動化は必須です.VSのソリューションファイルはビルドツール MSBuild の設定ファイルでもあります.そこでmonoのMSBuild互換ツール xbuild で,ビルドを自動化できるか検討しました.
結果は出来ません.理由はxbuild (というよりxbuildが読み込む microsoft.build.task.dll)が,ソースコードでmonoのコンパイラを設定するからです.MicorsoftのMSBuildのドキュメントには,プロパティ CscToolPath でコンパイラを指定できると書かれているのですが,xbuildはこのプロパティを見ておらずmonoのコンパイラをデフォルトで指定します.
Monotouchのコンパイルにはgmcsではなく/Developer/Monotouch/usr/bin/smcs (あるいはmoonlightのコンパイラそのものでもいいのかもしれませんが) を使わねばなりません.このパスが設定できないのでxbuildが利用できません.
そのために,ソースファイルおよびリソースファイルのリスト抜き出し処理をするコマンドライン プログラムを作り,それらのリストからのビルド,テストは make で行うことにしました.

処理フロー

Makefileのビルド,実行のフローは以下のとおりです:

  • リソースファイルのコピー. 画像ファイルなどをアプリのフォルダにコピーします.
  • ソースコードコンパイル
  • Info.plist ファイルの生成.
  • シミュレータの起動とアプリ実行.

csprojファイルからファイルリストを作るプログラムのコードは以下のとおりです:
$ コマンド csprojファイル名 > config.make
のように使います.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Xml.Linq;

namespace ConfigBuilder
{
    class Program
    {
        const string msbuild_ns = "http://schemas.microsoft.com/developer/msbuild/2003";
        static void Main(string[] args)
        {
            var xdoc = XDocument.Load(args[0]);
            
            var asmn = xdoc.Descendants(XName.Get("AssemblyName", msbuild_ns)).First<XElement>().Value;
            var dlls = from el in xdoc.Descendants(XName.Get("HintPath", msbuild_ns)) select el.Value;
            var srcs = from el in xdoc.Descendants(XName.Get("Compile", msbuild_ns))  select el.Attribute("Include");
            var ress = from el in xdoc.Descendants(XName.Get("Content", msbuild_ns))  select el.Attribute("Include");

            Console.WriteLine("#Configuration for the Makefile, generated from {0}.", args[0]);
            Console.WriteLine("CONFIG = DebugiPhoneSimulator");
            Console.WriteLine("ASSEMBLY_NAME = {0}", asmn);
            
            Console.Write("FILES = ");
            foreach (String s in srcs) Console.Write("\\\n\t{0}", s);
            Console.WriteLine();

            Console.Write("EXTRAS = ");
            foreach (String s in ress) Console.Write("\\\n\t{0}", s);
            Console.WriteLine();

            Console.Write("REFERENCES = ");            
            foreach (String s in dlls) Console.Write("\\\n\t{0}", s.Substring(0, s.Length - 4));
            Console.WriteLine(); 

            //Console.ReadKey();
        }
    }
}

これらのために2つのファイル,ファイルリスト config.make および Makefile を使います.
まずconfig.makeというファイルを作り,ファイルリストを書いていきます.最初のCONFIGだけは決めうち,あとの項目はそれぞれソリューションファイルの登録内容にあわせて記入します.

CONFIG        = DebugiPhoneSimulator
ASSEMBLY_NAME = VSSample_ScrollView
FILES         = Main.cs CustomScrollViewController.cs
EXTRAS        = thu_nanoha.jpg thu_riinzwei.jpg
REFERENCES    = monotouch

Makefile本体は以下のものになります.all でビルド,runでシミュレータで実行します.なぜかは分かりませんが,iPhone simulator が起動している状態ではrunしてもアプリが起動しません.このため必ずiPhone simulatorが終了した状態で,make runします.

# MonoTouch make file 
# rev 001 2009/10/23 A. Uehara

include config.make

srcdir=.
top_srcdir=..

# definitions
MT_TOOL_DIR = /Developer/MonoTouch/usr/bin
MT_LIB_DIR  = /Developer/MonoTouch/usr/lib/mono/2.1

CSC         = $(MT_TOOL_DIR)/smcs
MTOUCH      = $(MT_TOOL_DIR)/mtouch
MIBTOOL     = $(MT_TOOL_DIR)/mibtool
ARM_MONO    = $(MT_TOOL_DIR)/arm-darwin-mono
MONO        = $(MT_TOOL_DIR)/mono
MTOUCH_PACK = $(MT_TOOL_DIR)/mtouchpack

TOOL_DIR    = /usr/bin
RESGEN      = $(TOOL_DIR)/resgen
AL          = $(TOOL_DIR)/al

COMPILE_TARGET = exe

_pwd        = $(shell pwd)
_info_plist = $(APP_DIR)/Info.plist

# -- debug bild for an iphone simulator --
ifeq ($(CONFIG), DebugiPhoneSimulator)
 BUILD_DIR    = $(_pwd)/bin/iPhoneSimulator/Debug
 CSC_FLAGS    = -noconfig -codepage:utf8 -warn:4 -optimize- -debug "-define:DEBUG"
 ASSEMBLY_MDB = $(ASSEMBLY).mdb
 MTOUCH_FLAGS   = -v --nomanifest --nosign -sim "$(APP_DIR)" 
endif

# -- release build for an iphone simulator
ifeq ($(CONFIG), ReleaseiPhoneSimulator)
 BUILD_DIR    = $(_pwd)/bin/iPhoneSimulator/Release
 CSC_FLAGS    = -noconfig -codepage:utf8 -warn:4 -optimize-
 ASSEMBLY_MDB = 
 MTOUCH_FLAGS   = -v --nomanifest --nosign -sim "$(APP_DIR)" 
endif

# -- debug bild for an iphone
ifeq ($(CONFIG), DebugiPhone)
 BUILD_DIR    = $(_pwd)/bin/iPhone/Debug
 CSC_FLAGS    = -noconfig -codepage:utf8 -warn:4 -optimize- -debug "-define:DEBUG"
 ASSEMBLY     = bin/iPhoneStimulator/Debug/Sample_ScrollView.exe
 ASSEMBLY_MDB = $(ASSEMBLY).mdb
 MTOUCH_FLAGS   = -v --nomanifest --dev "$(APP_DIR) --certificate=$(CERTIFICATION) 
endif

# -- release build for an iphone
ifeq ($(CONFIG), ReleaseiPhone)
 BUILD_DIR    = $(_pwd)/bin/iPhone/Release
 CSC_FLAGS    = -noconfig -codepage:utf8 -warn:4 -optimize-
 ASSEMBLY_MDB = 
 MTOUCH_FLAGS   = -v --nomanifest --dev "$(APP_DIR)" --certificate=$(CERTIFICATION) 
endif

ASSEMBLY     = $(BUILD_DIR)/$(ASSEMBLY_NAME).$(COMPILE_TARGET)
APP_DIR      = $(BUILD_DIR)/$(ASSEMBLY_NAME).app
APP_ASSEMBLY = $(APP_DIR)/$(ASSEMBLY_NAME).$(COMPILE_TARGET)
CLEANFILES   = $(APP_DIR) $(ASSEMBLY)

build_references  := $(foreach item, $(REFERENCES), "/r:$(item).dll")
mtouch_references := $(foreach item, $(REFERENCES), -r="$(MT_LIB_DIR)/$(item).dll")
copy_contents := $(foreach item, $(EXTRAS), $(APP_DIR)/$(item) )

define _do_copy_contents
$1: $2
	@echo "Copying '$$<' to '$$@'"
	@mkdir -p '$$(shell dirname '$$@')'
	cp '$$<' '$$@'
# 	@test -z '$$<' || cp '$$<' '$$@'

endef


# rules
all:  $(copy_contents) $(APP_ASSEMBLY) $(_info_plist)

$(APP_ASSEMBLY): $(ASSEMBLY)
	mkdir -p $(APP_DIR)
	$(MTOUCH) $(MTOUCH_FLAGS) $(mtouch_references) "$(ASSEMBLY)"

$(ASSEMBLY): $(FILES) 
	mkdir -p $(shell dirname $(ASSEMBLY)) 
	$(CSC) $(CSC_FLAGS) -out:$(ASSEMBLY) -target:$(COMPILE_TARGET) $(build_references) $(FILES)

$(eval $(foreach item, $(EXTRAS), $(call _do_copy_contents, $(APP_DIR)/$(item), $(item) )))

clean:
	-rm -Rf $(CLEANFILES)

run: all
	$(MTOUCH) -launchsim=$(APP_DIR)

$(_info_plist): config.make
	mkdir -p $(shell dirname $(_info_plist)) 
	$(shell echo "<?xml version="1.0" encoding=\"utf-8\"?>" > $(_info_plist))
	$(shell echo "<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">" >> $(_info_plist))
	$(shell echo "<plist version=\"1.0\">" >> $(_info_plist))
	$(shell echo "  <dict>" >> $(_info_plist))

	$(shell echo "<key>CFBundleDevelopmentRegion</key><string>English</string>" >> $(_info_plist))
	$(shell echo "<key>CFBundleDisplayName</key><string>$(ASSEMBLY_NAME)</string>" >> $(_info_plist))
	$(shell echo "<key>CFBundleExecutable</key><string>$(ASSEMBLY_NAME)</string>" >> $(_info_plist))
	$(shell echo "<key>CFBundleIdentifier</key><string>com.yourcompany.$(ASSEMBLY_NAME)</string>" >> $(_info_plist))
	$(shell echo "<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>" >> $(_info_plist))
	$(shell echo "<key>CFBundleName</key><string>$(ASSEMBLY_NAME)</string>" >> $(_info_plist))
	$(shell echo "<key>CFBundlePackageType</key><string>APPL</string>" >> $(_info_plist))
	$(shell echo "<key>CFBundleSignature</key><string>????</string>" >> $(_info_plist))
	$(shell echo "<key>CFBundleSupportedPlatforms</key><array><string>iphonesimulator</string></array>" >> $(_info_plist))
	$(shell echo "<key>CFBundleVersion</key><string>1.0</string>" >> $(_info_plist))
	$(shell echo "<key>DTPlatformName</key><string>iphonesimulator</string>" >> $(_info_plist))
	$(shell echo "<key>DTSDKName</key><string>iphonesimulator3.0</string>" >> $(_info_plist))
	$(shell echo "<key>LSRequiresIPhoneOS</key><true />" >> $(_info_plist))

	$(shell echo "  </dict>" >> $(_info_plist))
	$(shell echo "</plist>" >> $(_info_plist))
サンプルコード

csprojファイルからリストを抜き出すのが
OneDrive
です.
上記のMakefileは雛形を兼ねてサンプルコードを作成しました.
OneDrive
これには,VSでコード補完するにはmonotouch.dllが必要ですが,2次配布はできないために,それだけは含んでいません.このフォルダにmonotouch.dllを含んでいなくてもビルドはできます.
ボタンを押すと画像が上下にスクロールして切り替わるだけのサンプルです.mini convertibleの写真を2次利用しています.比率がすごいことになっていますが,気にしないでください.

つまづいたところ

monoの環境で閉じたビルド環境にこだわり,xbuildのみで環境構築をしようとしてかなり時間を使いました.結局のところコンパイラのツールパス設定ができなことがソースコードを見て初めて分かりました.
それならCscタスクをパス設定できるものに変更してみたのですが,dllを作成してそれをGACに登録して,さらにxbuildのターゲットファイルにdll変更を反映させて,さらにxbuildの構文で上記Makefileの記述をするという,かなり手間のかかることになってしまいました.
GACのインストールという1点だけでも,方法として相当に一般性を失ってしまいました.もう1つの手としてxbuildから上記Makefile相当のシェルスクリプトを生成して実行するのも手だったのですが,それならばファイルリストだけxbuildで出して残りの作業はMakefileに記述したほうがよほどスマートと考えました.
そんなわけで,xbuildにこだわるこの方法は破棄し,上記Makefileに移った次第です.

改めて,注意点

改めて注意点をまとめておきます.このMakefileは基本的なことだけができる簡単なものです.以下のことは,できません:

  • xibおよびnibファイルの処理.
  • バイスでの実行は,私が環境を持っていないため,まだやっていません.デバイスでの実行に必須の署名の挿入などは,まだ対応していません.
  • テストするときにはiPhone simulatorを一旦終了させてください.iPhone simulatorが起動している状態からは,なぜか知りませんが,アプリが実行されません.

まとめ

VSを開発ツールにすえたビルド環境を構築しました.
xbuildのみでのビルド環境構築はコンパイラのパス指定ができないためにあきらめ,make で処理しました.まだxibファイルの処理,デバイスでの実行などをサポートしていません.
このビルド環境により,使い慣れたVSのソース編集環境を使いつつ,Macによるテストベッドで集中したビルドとテストを自動化して実行できます.