Scala で Android の世界に "こんにちは" をするまで。 なかなか自分の環境にズバリな情報が見付けられず試行錯誤したが、ようやく何とかなったのでまとめ。
とりあえず参考にしたページは以下。
ref. http://robey.lag.net/2009/01/19/scala-on-android.html
ref. http://www.scala-lang.org/node/160
ref. http://chneukirchen.org/blog/archive/2009/04/programming-for-android-with-scala.html
Thanks these authors.
最初に必要なもの。バージョンは今回わしが使ったもので、WindowsXP SP3 (cygwin 併用) で作業している。 あと空白入りパスでハマるのは嫌なので、あらかじめ全て空白が入らない場所に置いてある。
ProGuard はおそらく省略できそうなんだが、うちの貧弱マシンだといろいろと問題があって、これを使ったやり方しかうまくいかなかったので。 まあ、どっちにしろ使った方ができ上がるファイルは小さくなるのでおすすめ。 Android SDK はもたもたしてる間に 1.6 が出てしまったが、とりあえず 1.5 で。 1.6 で手順を変更する必要がある場合は後日フォローしたい。
Eclipse は貧弱マシンなので (というかそもそもあんまり好きじゃないし) 使わない。
まず Scala のソースを SVN か リポジトリスナップショット (ダウンロードページにある) で取得。 プリビルド版の Scala の src には android-library が含まれていないので注意。
% wget http://www.scala-lang.org/downloads/distrib/files/scala-2.7.6.final-sources.tgz % tar zxf scala-2.7.6.final-sources.tgz
次に既存の scala-library.jar をどこかに展開し、android 用の部分だけ差し換える。 参考にしたページでは scala-library のソースを上書きして丸ごとビルドしろと書かれているが、 うちのボロマシンでは無理なのでビルド済みの jar をいじる。 実際に scala-android のディレクトリを見てみればわかるが、scala-library との差分はファイル二つだけ。
% mkdir workdir; cd workdir
% jar xf ${SCALA_HOME}/lib/scala-library.jar
% export SRC=${scala-src-path}/src/android-library
% scalac ${SRC}/scala/ScalaObject.scala
% scalac ${SRC}/scala/reflect/ScalaBeanInfo.scala
% jar cf ../scala-android.jar *
展開した場所で scalac を実行すればうまいこと該当するクラスファイルだけが上書きされるはず。 で、それを再度 jar で固めれば終了。
今回はこれを ${SCALA_HOME}/android-lib/scala-android.jar という場所に置くことにする。
プロジェクトの作成に関してはマニュアルが日本語化されているので問題無いだろう。
ref. http://developer.android.com/intl/ja/guide/developing/other-ide.html
とりあえず例としてはこんな感じ。 あらかじめ Android SDK の tools ディレクトリを PATH に含めてある。
% android.bat create project --target 2 --path ./hello_scala --activity HelloScala --package com.azito.jijixi
こうすると、Android 1.5 用の (android.bat list target 参照) プロジェクトファイルが ./hello_scala 以下に作成される。 activity というのは Android アプリケーションのエントリーポイントになるクラス。 上記のように実行すると src/com/azito/jijixi/HelloScala.java というファイルができているはず。 まあ今回は必要無いのでさくっと消しちゃっても良い。
プロジェクトディレクトリに移り、build.xml に以下のパッチをあてる。 ほんとは完全に別ファイルにして build.xml から読み込むだけにしたかったんだけど、Ant の知識が足りなくて断念。
% cat ${where-place}/build.xml.diff
--- build.xml.orig 2009-09-15 19:35:12.781250000 +0900
+++ build.xml 2009-09-16 17:11:24.843750000 +0900
@@ -58,4 +58,89 @@
targets are used.
-->
<setup />
+
+ <!-- for Scala settings. -->
+ <!-- set properties for your evnvironment. -->
+ <property name="scala-home"
+ value="c:/develop/scala" />
+ <property name="scala-android-library"
+ value="${scala-home}/android-lib/scala-android.jar" />
+ <property name="proguard-home"
+ value="c:/develop/proguard" />
+
+ <!-- override targets in platforms/android-1.5/templates/android_rules.xml -->
+ <target name="compile" depends="dirs, resource-src, aidl">
+ <javac encoding="ascii" target="1.5" debug="true" extdirs=""
+ includes="**/*.java"
+ destdir="${out-classes}"
+ bootclasspathref="android.target.classpath">
+ <src path="${source-folder}"/>
+ <src path="${gen-folder}"/>
+ <classpath>
+ <pathelement path="${scala-android-library}"/>
+ <fileset dir="${external-libs-folder}" includes="*.jar"/>
+ <pathelement path="${main-out-classes}"/>
+ </classpath>
+ </javac>
+
+ <taskdef resource="scala/tools/ant/antlib.xml">
+ <classpath>
+ <pathelement path="${scala-home}/lib/scala-compiler.jar"/>
+ <pathelement path="${scala-home}/lib/scala-library.jar"/>
+ </classpath>
+ </taskdef>
+
+ <scalac force="changed" deprecation="on"
+ includes="**/*.scala"
+ destdir="${out-classes}"
+ bootclasspathref="android.target.classpath">
+ <src path="${source-folder}"/>
+ <classpath>
+ <pathelement path="${scala-android-library}"/>
+ <fileset dir="${external-libs-folder}" includes="*.jar"/>
+ <pathelement path="${main-out-classes}"/>
+ </classpath>
+ </scalac>
+ </target>
+
+ <property name="stripped-jar-file"
+ value="${out-classes-location}/../classes.stripped.jar" />
+
+ <target name="dex" depends="proguard">
+ <echo>Converting compiled files and external libraries into ${out-folder}/${dex-file}...</echo>
+ <apply executable="${dx}" failonerror="true" parallel="true">
+ <arg value="--dex" />
+ <arg value="--output=${intermediate-dex-location}" />
+ <fileset file="${stripped-jar-file}" />
+ </apply>
+ </target>
+
+ <target name="proguard" depends="compile">
+ <taskdef resource="proguard/ant/task.properties"
+ classpath="${proguard-home}/lib/proguard.jar" />
+
+ <fileset id="external-jars"
+ dir="${external-libs-folder}" includes="**/*.jar" />
+ <pathconvert property="external-jars" refid="external-jars" />
+
+ <proguard>
+ <injar path="${out-classes-location}" />
+ <injar path="${scala-android-library}:${external-jars}"
+ filter="!META-INF/MANIFEST.MF,!library.properties" />
+ <outjar path="${stripped-jar-file}" />
+ <libraryjar path="${android-jar}" />
+ <keep name="**.R" />
+ <keep name="**.R$*" />
+ <keep name="org.xml.sax.EntityResolver" /> <!-- BK :-p -->
+ -dontwarn
+ -dontoptimize
+ -dontobfuscate
+ -keep public class * extends android.app.Activity
+ </proguard>
+ </target>
+
+ <!-- for debug. -->
+ <target name="show-prop">
+ <echoproperties />
+ </target>
</project>
% cd hello_scala
% patch < ${where-place}/build.xml.diff
patching file build.xml
パッチをあてたら必要に応じて新しい build.xml の以下の箇所を編集。 まあパッチの方を直しちゃった方が早いかもしれないが、その辺は好き好きで。
<property name="scala-home"
value="c:/develop/scala" />
<property name="scala-android-library"
value="${scala-home}/android-lib/scala-android.jar" />
<property name="proguard-home"
value="c:/develop/proguard" />
scala-home は Scala をインストールしてある場所。 scala-android-library は先に作った Android 用 scala-library.jar の場所。 proguard-home は ProGuard をインストールしてある場所。 他の場所については基本的にいじる必要が無いようになってるはず。 (だが、知識不足は否めないのであまり信用しないように)
src/com/azito/jijixi/HelloScala.java を消して代わりに src/com/azito/jijixi/HelloScala.scala を作成。 とりあえず以下のような感じで。 ProGuard が必要無いクラスを消してしまうので、ある程度 Scala 的な操作を入れないと出来上がりがつまらなくなる。 ということで、ちょっと無駄に List とか使ってある。
% cat src/com/azito/jijixi/HelloScala.scala
package com.azito.jijixi
import _root_.android.app.Activity
import _root_.android.os.Bundle
import _root_.android.widget.TextView
class HelloScala extends Activity {
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
val tv = new TextView(this)
val lst = List("Hello", "Android,", "it's", "me,", "Scala!")
tv.setText(lst mkString(" "))
setContentView(tv)
}
}
できたらこれをビルド。
% ant debug
Buildfile: build.xml
[setup] Project Target: Android 1.5
[setup] API level: 3
dirs:
[echo] Creating output directories if needed...
[mkdir] Created dir: c:\home\jijixi\tmp\android_test\hello_scala\bin\classes
resource-src:
[echo] Generating R.java / Manifest.java from the resources...
aidl:
[echo] Compiling aidl files into Java classes...
compile:
[javac] Compiling 1 source file to c:\home\jijixi\tmp\android_test\hello_scala\bin\classes
[scalac] Compiling 1 source file to c:\home\jijixi\tmp\android_test\hello_scala\bin\classes
[scalac] Element 'c:\home\jijixi\tmp\android_test\bin\classes' does not exist.
proguard:
[proguard] ProGuard, version 4.4
[proguard] Reading program directory [C:\home\jijixi\tmp\android_test\hello_scala\bin\classes]
[proguard] Reading program jar [C:\develop\scala\android-lib\scala-android.jar] (filtered)
[proguard] Reading library jar [C:\develop\android-sdk\platforms\android-1.5\android.jar]
[proguard] Note: You're ignoring all warnings!
[proguard] Preparing output jar [C:\home\jijixi\tmp\android_test\hello_scala\bin\classes.stripped.jar]
[proguard] Copying resources from program directory [C:\home\jijixi\tmp\android_test\hello_scala\bin\classes]
[proguard] Copying resources from program jar [C:\develop\scala\android-lib\scala-android.jar] (filtered)
dex:
[echo] Converting compiled files and external libraries into bin/classes.dex...
package-resources:
[echo] Packaging resources
[aaptexec] Creating full resource package...
debug:
[apkbuilder] Creating HelloScala-debug.apk and signing it with a debug key...
[apkbuilder] Using keystore: C:\Documents and Settings\jijixi\.android\debug.keystore
BUILD SUCCESSFUL
Total time: 46 seconds
エラーが出なければ完了。 一応 ProGuard が出力したファイルの中身なんかを見てみると、
% jar tf bin/classes.stripped.jar com/azito/jijixi/HelloScala.class com/azito/jijixi/R$attr.class com/azito/jijixi/R$layout.class com/azito/jijixi/R$string.class com/azito/jijixi/R.class scala/$colon$colon.class scala/Array$Array0$class.class scala/Array$Array0.class scala/Array$ArrayLike.class scala/Array.class scala/collection/mutable/Buffer$$anonfun$$plus$plus$eq$1.class scala/collection/mutable/Buffer$class.class scala/collection/mutable/Buffer.class scala/collection/mutable/CloneableCollection$class.class scala/collection/mutable/CloneableCollection.class scala/collection/mutable/ListBuffer$$anon$1.class scala/collection/mutable/ListBuffer$$anonfun$equals$1.class scala/collection/mutable/ListBuffer.class scala/Collection$class.class scala/Collection.class scala/compat/Platform$.class scala/Function1$class.class scala/Function1.class scala/Iterable$class.class scala/Iterable.class scala/Iterator$$anon$17.class scala/Iterator$class.class scala/Iterator.class scala/List$$anon$2.class scala/List$.class scala/List.class scala/MatchError.class scala/Math$.class scala/Nil$.class scala/PartialFunction$class.class scala/PartialFunction.class scala/Product$class.class scala/Product.class scala/Product2$class.class scala/Product2.class scala/RandomAccessSeq$class.class scala/RandomAccessSeq$Mutable$class.class scala/RandomAccessSeq$Mutable.class scala/RandomAccessSeq.class scala/runtime/BoxedArray$AnyIterator.class scala/runtime/BoxedArray.class scala/runtime/BoxedObjectArray.class scala/runtime/BoxedUnit.class scala/runtime/BoxesRunTime.class scala/runtime/Nothing$.class scala/runtime/ScalaRunTime$.class scala/Seq$class.class scala/Seq.class scala/StringBuilder$.class scala/StringBuilder.class scala/Tuple2.class
List を使っただけでこんなに関係するのか…… と思いつつも、元の jar が 3.7MB くらいあるのが 60KB 弱にまで減るんだからありがたいことだ。 最終的にはこれがさらに圧縮されて 18KB ほどになる (apk ファイル)。 ProGuard を使わない場合は 800KB くらいらしい。 (うちでは dx が謎のエラーで死んでしまうので実際には試せていないが)
省略。 すでに紹介したが、
ref. http://developer.android.com/intl/ja/guide/developing/other-ide.html#Running
こちらのページの「アプリケーションの実行」という項を見ればとてもよくわかるはず。 インストールするとアプリケーションタブに HelloScala というアイコンが現れているはずなので、 そいつを起動して "Hello Android, it's me, Scala!" と表示されれば成功。