create_projectを改造してみた

(この記事は Scala Advent Calendar jp 2010 : ATND の7日目です。)

sbt-android-pluginのcreate_projectスクリプトを改造してみました。
こちら → https://gist.github.com/738232

sbt-idea を利用してIntelliJ IDEAで開発できるようにします。

使い方

プロジェクト名とpackage
$ create_project HelloProject com.example.hello

これは元のcreate_projectとは挙動が違って、sbtプロジェクトがないところでsbtコマンド打った感じになります。
(オプション指定はスクリプトいじってください)

Scalaバージョン指定
$ create_project HelloProject com.example.hello --scala-version 2.8.1

デフォルトで実行環境のバージョンになる、かな?

Android用プロジェクト
$ create_project HelloProject com.example.hello --android

デフォルトの設定でAndroidプロジェクトを作成します。
元のcreate_projectで名前とpackageだけ指定したのと大体同じ。

Android / APIレベル、platform、Activity
$ create_project HelloProject com.example.hello --api-level 8 --platform android-8 --activity HelloActivity

元と同じ。この辺指定する場合は --android つけなくていいです。
--api-level は --platform を上書きするので注意です。(例の場合、--platform android-8 は不要)

sbt-ideaを利用
$ create_project HelloProject com.example.hello --idea

sbt-ideaを利用するための設定を行います。
Plugins.scalaに書き足したり、Project.scalaでIdeaProjectミックスインしたり、idea.properties作成したり。

idea.propertiesの設定
--project-jdk-name <name>
--project-output-path <path>
--java-language-level <level>
--exclude-sbt-project-definition-module
--exclude-libmanaged-folders
--compile-with-idea

この辺指定する場合は --idea つけなくていいです。
--exclude-sbt-project-definition-module は include.sbt.project.definition.module=false にします。
それぞれの項目についてはsbt-ideaのREADMEを参照してください。
(--project-output-pathはちゃんと動かないかも。idea.propertiesだけじゃなくProject.scalaも変えなきゃいけないと思うけど時間足りなかった)

.gitignore生成
$ create_project HelloProject com.example.hello --gitignore

プロジェクトのルートに .gitignoreファイルを作ります。
内容変えたい場合はスクリプトいじってください。

要するに
$ create_project HelloProject com.example.hello --android --idea --gitignore
$ cd HelloProject
$ sbt update
$ sbt idea

これでIDEAで開けるAndroidプロジェクトを作れるよー。ついでに.gitignoreも作っときます。
というスクリプト。

ついでに

IDEAから開く場合、

$ sbt compile

を実行した後、TR.scalaというファイルが生成されるので
src_managed/main/scala をソースフォルダとして指定してください。(画像参照)
f:id:kxbmap:20101213032448p:image

注意

Cygwin用だよ
#!/bin/sh
exec scala -deprecation `cygpath -m $0` "$@"
!#

Cygwin用に書き換えてあります。そんなもん使わねーよって人は戻してください。

Scala2.8↑用だよ
c.copy(project = c.project.copy(scalaVersion = i.next()))

こういうコードがあるのでスクリプト実行する環境は2.8じゃないといけません多分。

sbt0.7.4用だよ

中身書き換えたら大丈夫かもしれないけど、0.7.5は試してません。

AndroidSDKは最新(2010/12/13現在)のだよ
def androidPlatformToolsPath = androidSdkPath / "platform-tools"
override def adbPath = androidPlatformToolsPath / adbName

最新のSDKでadb.exeの位置が変わってるので、↑を定義してあります。これ消せば最新じゃなくても動くはず。

でもandroid-9には対応してないよ

すみません。

IntelliJ IDEA 9 用だよ

Android使う場合。10だとうまくいかなかった。10でAndroidサポート入ってるので干渉してるかもしれない。
Android使わなければ10で大丈夫。

スクリプトの中身の話

重要そうなところだけ。
中身は大きく分けてコマンド引数を解釈して設定を作成する部分と、設定を元に必要なファイル郡を生成する部分の2つに分かれます。

config
def config(i: Iterator[String]) = {
  @annotation.tailrec
  def cfg(i: Iterator[String], c: Config = null): Option[Config] =
//...
  cfg(i)
}

コマンド引数を解釈する再帰関数。元のスクリプトは引数をループで回してvarを書き換えるという方法でやっていたのでScalaらしく?してみました。
cが状態変数ですね。@tailrec便利です。(参照: Scala Advent Calendar 2010 [Day 3]再帰再入門
呼び出しごとにIteratorを1つ(or 2つ)進めて、解釈したものをcase classのcopyメソッドを使って状態変数に反映、次が無くなるまで再帰呼び出しします。
成功したらcをSomeに包んで、失敗したらNoneを返します。

val name = i.next()
if ((name matches """[a-zA-Z]\w+""" tap { b => if(!b) println("invalid project name: "+name) }) && i.hasNext){
  val pkg = i.next()
  if (pkg matches """[a-z]+\.([a-z]+\.?)+[a-z]$""") cfg(i, Config(ProjectConfig(name, name, pkg)))
  else None tap { _ => println("invalid package name, need 2 package components (e.g. com.bar)") }
} else None

ごちゃってますが途中でエラーメッセージを出すために tap というのも使ってみました。
(参照: Scala Advent Calendar 2010 [Day 4]どうしても副作用があるコードを書かないといけない場合に使うと便利かもしれないもの

def setAndroid(c: Config)(f: AndroidConfig => AndroidConfig):Config =
  c.copy(android = Some(f(c.android getOrElse AndroidConfig())))
//...
case "--platform" =>
  if (!i.hasNext) None else cfg(i, setAndroid(c) { _.copy(platform = i.next()) })

このような具合で、copyメソッドは一部だけ内容を変えたオブジェクトを簡単に作れるので便利です。

generate

設定を元にファイル郡を生成する関数。時間が足りなくて元からあんまり変わってないです。
Androidプロジェクトじゃなくても使えるようにしたいので、Project.scalaで継承・ミックスインするものを設定によって変えるようにしてあります。

val mainMix = {
  val buf = new ListBuffer[String]
  buf += (if (withAndroid) "AndroidProject(info)" else "DefaultProject(info)")
  if (withAndroid) {
    buf += "Defaults"
    buf += "MarketPublish"
    buf += "TypedResources"
  }
  if (withIdea) buf += "IdeaProject"
  buf.toList
} mkString ("extends ", " with ", "")

非常に手続き的ですが……
ListBufferで必要なもののリストを作って、mkStringで連結します。

"extends AndroidProject(info) with Defaults with MarketPublish with TypedResources with IdeaProject"

こんな文字列ができます。
TypedResourcesについてはこちらの記事が詳しいです。
sbt-android-pluginで型安全にリソース取得 - papamitra

object CreateProject

sbt-android-plugin | ブログ.武田ソフト.jp こちらの記事を参考にして、全体をobjectで包んでいます。
object定義の前と後を削除してコンパイルしたらscalaコマンドから使えるんじゃないかな。

おわりに

こんな記事を最後まで読むほどScalaに興味がある人は Scala Advent Calendar jp 2010 : ATND に参加するといいと思います。
現時点であと2人空いてるようですよー。