smallgeek

Swing god gun, I need it low demon

Xamarin.Forms 用の fsproj をビルドしたときに遭遇したバグの話

はじめに

この記事で扱われるバグは Alpha preview で修正されています。記事を書いている間に修正されたことを記事を公開する直前に気づいて枕を濡らしました。

概要

F# で Xamarin.Forms 向けのプロジェクトを作成しようとしたらビルドエラーになったので調査した話。コードはほとんど出てきませんが無駄に長いです。

前書き

現在は F# の Xamarin.Android や Xamarin.iOS 向けのプロジェクトテンプレートが提供されていて、昔に比べると開発が容易になったと感じます。そんなプロジェクトテンプレートですが、残念なことに F# の Xamarin.Forms 向けは提供されていません。GitHub で見かける F# + Xamarin.Forms のサンプルは画面回りが C# で、共通部分が F# になっているものばかりです。F#er なら全部 F# にしたい。そこで今回は、Xamarin Forms のプロジェクトをすべて F# で作ってからアプリケーションを開発することに挑戦します。

手順

  1. 前述のプロジェクトテンプレートを使用して、それぞれのプロジェクトを作成します。次に PCL 側を追加し、Android / iOS プロジェクトから参照します。
  2. paket を使用して作成したプロジェクトに Xamarin.Forms パッケージを参照させます。 paket の使い方についてはこちらの素晴らしい記事 Paketコトハジメ #FsAdvent を参考にしてください。
  3. 各プラットフォームで Xamarin.Forms.Forms.Init を呼び出します。
// Android
[<Activity (Label = "Sample.Droid", MainLauncher = true)>]
type MainActivity () =
    inherit Xamarin.Forms.Platform.Android.FormsApplicationActivity()

    override this.OnCreate (bundle) =

        base.OnCreate (bundle)

        Xamarin.Forms.Forms.Init(this, bundle)
        this.LoadApplication(new App());
[<Register ("AppDelegate")>]
type AppDelegate () =
    inherit Xamarin.Forms.Platform.iOS.FormsApplicationDelegate()

    // This method is invoked when the application is ready to run.
    override this.FinishedLaunching (app, options) =

        Xamarin.Forms.Forms.Init()

        this.LoadApplication(App())

        base.FinishedLaunching(app, options)

あとは Visual Studio または Xamarin Studio を使用してビルドします。

Droid

いざビルドしてみても失敗してしまいます。ログを確認してみます。

エラー FS0010: 予期しない キーワード 'end' です メンバー定義内
エラー FS0010: この場所またはその前にある構造化コンストラクトが不完全です 実装ファイル内。このポイントまたはその前にある構造化コンストラクトが不完全です または他のトークンを指定してください。
警告 FS0046: 識別子 'fixed' は F# で将来利用するために予約されています

Resource.designer.fs でエラーが発生していました。

// aapt resource value: 0x7f07001c
static member end = 2131165212
            
// aapt resource value: 0x7f070098
static member end_padder = 2131165336

F# の予約語である end がエスケープされていないためエラーになっています。このファイルは Xamarin.Android が自動生成するため、自分のプログラミングミスではありません。そもそもまだコードを書いていません。どうやら Xamarin.Forms が依存するパッケージに含まれるリソースを Resource.designer.fs に出力する処理に問題があるようです。

Google it!

困ったのでぐぐると issue が見つかりました。しかしこのバグは修正済みだと Don Syme 氏が仰っていました。Bug: The end keyword is not escaped in generated codedom #10

RELEASE_NOTES によると、0.9.4.0 で修正されているとのことです。

バグの報告元では榎本さんが参照を更新したと仰っていました。24709 – F# reserved keyword 'end' being generated in member for resource.designer.fs

しかし私の環境ではエスケープされていません。再インストールしても、alpha チャンネルにしても同じ現象が発生しています。

DLL を確認する

FSharp.Compiler.CodeDom.dll を確認します。Windows だと %ProgramFiles(x86)%MSBuild\Xamarin\Android に、OS X だと /Library/Frameworks/Xamarin.Android.framework/Versions/{バージョン}/lib/xbuild/Xamarin/Android に格納されています。

$ monodis --assembly /Library/Frameworks/Xamarin.Android.framework/Versions/7.0.2-37/lib/xbuild/Xamarin/Android/FSharp.Compiler.CodeDom.dll 
Assembly Table
Name:          FSharp.Compiler.CodeDom
Hash Algoritm: 0x00008004
Version:       0.9.2.0
Flags:         0x00000000
PublicKey:     BlobPtr (0x00000000)
    Zero sized public key
Culture:  

エスケープが Fix されたバージョンよりも古い DLL が格納されていました。

コードを確認する

Xamarin.Androidオープンソースソフトウェアですので、コードを参照できます。ビルド関連のコードを探すと、ResourceDesigner を生成するコードが見つかりました。FSharp.Compiler.CodeDom.FSharpCodeProvider も使用されています。

実験

FSharp.Compiler.CodeDom.dll を入れ替えてみましょう。Xamarin.Android.Build.Tasks.dll が参照している FSharp.Compiler.CodeDom.dllFSharp.Compiler.CodeDom, Version=0.9.2.0, Culture=neutral, PublicKeyToken=null であるため、バージョンと PublicKeyToken を合わせる必要があります。

fsprojects/FSharp.Compiler.CodeDom から clone してビルドします。dll ができあがったら、先ほどのフォルダにある dll と入れ替えます。その後 Droid プロジェクトをビルドすると、なんと成功しました。再生成された Resource.designer.fs 内のキーワードもエスケープされています。ということは、この FSharp.Compiler.CodeDom.dll をどうにかしてやればビルドが通るようになるはずです。

オープンソース版の xamarin-android をビルドする

であれば、オープンソース版を自分でビルドして参照してやればよさそうです。製品版の dll をいじるのはいろいろと問題がありますし。手始めに xamarin/xamarin-android からリポジトリを clone します。あとは git submodule update --init --recursive make prepare make とコマンドを実行すればよいはずですが、ubuntu だとうまくいかなかったので今回は OS X でビルドしました。

targets の import 先を変更する

出来上がった生成物の中に Xamarin.Android.FSharp.targets が含まれていますので、Droid プロジェクトの Import 先を変更します。

<Import Project="..\..\..\path\to\Xamarin.Android.FSharp.targets" />

Build

ここまで来たらビルド可能になっているはずなので、コマンドを実行してビルドを走らせます。

xbuild Sample.Droid.fsproj /p:AndroidSupportedAbis=armeabi-v7a,x86 /t:SignAndroidPackage
...
Build succeeded

ビルドに成功しました。apk ファイルも生成されています。

Debug

出来上がったアプリを実行してもすぐに落ちるので IDE を使用してデバッグ実行してみたのですが、エラーが発生します。

f:id:smallgeek:20161201235944p:plain

Xamarin Studio だとこれといったエラーログは出力されないのですが、Visual Studio だと Xamarin.Android.Build.Debugging.Tasks.dll が見つからなかったと出力されます。xamarin-android の生成物に Xamarin.Android.Build.Debugging.Tasks.dll は含まれていないので、製品版の DLL を使用してみましたがうまくいかず。Stable に修正が反映されるのを待ちましょう。

iOS

iOS 向けプロジェクトは何もしなくてもビルドできてシミュレーターでも動作しています。テンプレートそのままなので当然なのですが何もエラーがなくて逆に怖いですね。

f:id:smallgeek:20161201232613p:plain

余談

xamarin-androidリポジトリを観察しているとこういうのも見つかりました。これは F# の TypeProvider という機能を使用してリソースを生成するライブラリのようです。 github.com

TypeProvider については恐らく F# level-9 ninja の方が記事を書いてくれると思います。

まとめ

Xamarin はオープンソースソフトウェアですので、情報を収集しやすく、自分で調査・実験することもできます。自分の使用するソフトウェアをビルドしてみるとわかることがたくさんあります。Xamarin は更新が早いのでアップデート内容は早めに確認しましょう。

参考

grimoire of Android