build.gradle.ktsへの書き換えとハマリポイント part 1

## はじめに

KotlinConfで `build.gradle` をktsで書いてバリバリ補完効かせながら書いてるのを見てよさそうじゃんって試してみたらめちゃめちゃハマったのでメモです。

### 環境

- Android Studio 3.0
    - Gradle wrapper 4.3.1
- Mac OS Sierra

## サンプルプロジェクト

こんな感じのサンプルを書き換えていきます。

```groovy:build.gradle
buildscript {
    ext.kotlin_version = '1.2.0-rc-39'
    repositories {
        google()
        jcenter()
        maven { url 'http://dl.bintray.com/kotlin/kotlin-eap-1.2' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'com.google.gms:google-services:3.1.2'
        classpath 'com.github.gfx.ribbonizer:ribbonizer-plugin:2.1.0'
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        maven { url 'http://dl.bintray.com/kotlin/kotlin-eap-1.2' }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
```

```groovy:app/build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'com.github.gfx.ribbonizer'

android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "com.chibatching.ktssample"
        minSdkVersion 17
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        debug {
            minifyEnabled false
            applicationIdSuffix '.debug'
        }
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

ribbonizer {
    builder { variant, iconFile ->
        if (variant.buildType.name == "debug") {
            return yellowRibbonFilter(variant, iconFile)
        } else {
            return grayRibbonFilter(variant, iconFile)
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlinVersion"

    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'

    implementation 'com.google.firebase:firebase-core:11.6.0'

    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}

apply plugin: 'com.google.gms.google-services'
```

## Step 1 - ルートレベルのbuild.gradle

分量的にも難易度的にもとりあえずここからやるのがいいと思います。

素直に変換すると次のような感じになります。ほとんど同じですね。
大きく違うのはgroovyの時に `ext.kotlin_version = '1.2.0-rc-39'` としていた箇所。
delegated propertyを使うことで(gradleプロジェクトの)拡張プロパティを定義することができます(ついでに名前をキャメルケースに変更したのでappレベルのほうも適宜変更してください)。

```kotlin:build.gradle.kts
buildscript {
    val kotlinVersion by extra { "1.2.0-rc-39" }
    repositories {
        google()
        jcenter()
        maven(url = "http://dl.bintray.com/kotlin/kotlin-eap-1.2")
    }
    dependencies {
        classpath("com.android.tools.build:gradle:3.0.0")
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
        classpath("com.google.gms:google-services:3.1.2")
        classpath("com.github.gfx.ribbonizer:ribbonizer-plugin:2.1.0")
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        maven(url = "http://dl.bintray.com/kotlin/kotlin-eap-1.2")
    }
}

task("clean", Delete::class) {
    delete = setOf(rootProject.buildDir)
}
```

### ハマリポイント 1: gradle syncできない

ここで一発でgradle syncできたあなた！ラッキーですね！
僕は出来ませんでした。一番ハマったのがここです。
しかも、`./gradlew` コマンドからはビルドできるのにAndroid Studioではsyncできないという状況。
KotlinConf会場のGradleブースで質問できていなかったら多分諦めてます。

ASのGradle Consoleには以下のようなエラーが出ていました。

```
Error:Cause: org/jetbrains/kotlin/lexer/KotlinLexer : Unsupported major.minor version 52.0
```

はい、後半部分はJavaで開発してるとわりとよく見るやつですね。Javaのバージョンが低いです。
ちなみに、build.gradle.ktsはJava 8以上が必要です。

コマンドラインからはビルドできているので `./gradlew -v` しても当然Java 8だと表示されます。

```
------------------------------------------------------------
Gradle 4.3.1
------------------------------------------------------------

Build time:   2017-11-08 08:59:45 UTC
Revision:     e4f4804807ef7c2829da51877861ff06e07e006d

Groovy:       2.4.12
Ant:          Apache Ant(TM) version 1.9.6 compiled on June 29 2015
JVM:          1.8.0_131 (Oracle Corporation 25.131-b11)
OS:           Mac OS X 10.12.6 x86_64
```

ASが内部で使うJavaのバージョンが悪いと想像はつくのですがASにGradleが使うJVMの設定はありません。

<img width="1134" alt="スクリーンショット 2017-11-13 2.19.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/53706/cafe9397-411b-033f-a4cd-d7ea80c6b3aa.png">

Gradleの中の人に助けてくれーと泣きついて出てきた解決策は「一度IntelliJ IDEAで開いて設定を変える」でした。
Ultimate版で試したので確認はしていないですがCommunity版にも多分あるんじゃないでしょうか。(雑ですいません)

<img width="1387" alt="スクリーンショット 2017-11-13 2.19.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/53706/fc3869fc-75f3-c2d6-3b40-2d0f02877ab4.png">

ここでGradle JVMの項目を1.8に変更します。その後、ASで開き直すとGradle syncできるようになります。

.ideaフォルダのファイルに[差分](https://github.com/chibatching/KtsSample/commit/99d26696b1f174d9515fccbd6387686d5dc94527#diff-6e000adeb9c421062e2fb3f6513de99c)が出ていたので `.idea/gradle.xml` の編集だけでも大丈夫なのではと思いますが未確認です。

## Step 2 - appレベルのbuild.gradle

気合でガッと全部書き換えます(徐々に変えていく方法もあります - [appendix](https://qiita.com/chibatching/items/d63c0c9afc8e1e6bc9de#appendix))
ただ、appレベルを書き換える際、ルートレベルのbuild.gradleとsettings.gradleも変更する必要があります。

最初にこのstepでの最終型を示してハマリポイントを紹介していこうと思います。

```kotlin:build.grale.kts
allprojects {
    val kotlinVersion by extra { "1.2.0-rc-39" }
    repositories {
        google()
        jcenter()
        maven(url = "http://dl.bintray.com/kotlin/kotlin-eap-1.2")
    }
}

task("clean", Delete::class) {
    delete = setOf(rootProject.buildDir)
}
```

```kotlin:app/build.gradle.kts
import com.github.gfx.ribbonizer.FilterBuilder
import com.github.gfx.ribbonizer.GrayRibbonBuilder
import com.github.gfx.ribbonizer.YellowRibbonBuilder

plugins {
    id("com.android.application") version "3.0.0"
    kotlin("android") version "1.2.0-rc-39"
    kotlin("android.extensions") version "1.2.0-rc-39"
    id("com.github.gfx.ribbonizer") version "2.1.0"
    id("com.google.gms.google-services") version "3.1.2" apply false
}

android {
    compileSdkVersion(26)
    defaultConfig {
        applicationId = "com.chibatching.ktssample"
        minSdkVersion(17)
        targetSdkVersion(26)
        versionCode = 1
        versionName = "1.0"
        testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        getByName("debug") {
            isMinifyEnabled = false
            applicationIdSuffix = ".debug"
        }
        getByName("release") {
            isMinifyEnabled = true
            proguardFiles(getDefaultProguardFile("proguard-android.txt"), file("proguard-rules.pro"))
        }
    }
}

ribbonizer {
    withGroovyBuilder {
        "builder"(FilterBuilder { variant, iconFile ->
            return@FilterBuilder when (variant.buildType.name) {
                "debug" -> YellowRibbonBuilder().apply(variant, iconFile)
                else -> GrayRibbonBuilder().apply(variant, iconFile)
            }
        })
    }
}

val kotlinVersion: String by extra

dependencies {
    implementation(fileTree(mapOf("dir" to "libs", "include" to arrayOf("*.jar"))))
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlinVersion")

    implementation("com.android.support:appcompat-v7:26.1.0")
    implementation("com.android.support.constraint:constraint-layout:1.0.2")

    implementation("com.google.firebase:firebase-core:11.6.0")

    testImplementation("junit:junit:4.12")
    androidTestImplementation("com.android.support.test:runner:1.0.1")
    androidTestImplementation("com.android.support.test.espresso:espresso-core:3.0.1")
}

apply(mapOf("plugin" to "com.google.gms.google-services"))
```

```groovy:settings.gradle
pluginManagement {
    repositories {
        gradlePluginPortal()
        maven { url "https://jcenter.bintray.com/" }
        maven { url "https://maven.google.com" }
        maven { url "http://dl.bintray.com/kotlin/kotlin-eap-1.2" }
    }
    resolutionStrategy {
        eachPlugin {
            switch (requested.id.id) {
                case "com.android.application":
                    useModule("com.android.tools.build:gradle:${requested.version}")
                    break
                case "com.github.gfx.ribbonizer":
                    useModule("com.github.gfx.ribbonizer:ribbonizer-plugin:${requested.version}")
                    break
                case "com.google.gms.google-services":
                    useModule("com.google.gms:google-services:${requested.version}")
                    break
            }
        }
    }
}

include ':app'
```

### ハマリポイント 2: apply構文が(実質)使えない

appレベルのgradleの冒頭、見慣れない記述があります。

```kotlin:app/build.gradle.kts
plugins {
    id("com.android.application") version "3.0.0"
    kotlin("android") version "1.2.0-rc-39"
    kotlin("android.extensions") version "1.2.0-rc-39"
    id("com.github.gfx.ribbonizer") version "2.1.0"
    id("com.google.gms.google-services") version "3.1.2" apply false
}
```

build.gradle.ktsでは、プラグインの適用を今までのapply構文ではなくこのplugins構文で行う必要があります。

apply構文も実際には使えるのですが、その場合各プラグインのconfigurationにtype safeにアクセスができません。
つまり `android { ...` 等と書いてもすべてコンパイルエラーになります。

このplugins構文ではプラグインの依存解決は`buildscript`ブロックではなく、settings.gradleの`pluginManagement`で行うことになります(上記サンプル参照)。
この`pluginManagement`の`repositories`ブロックで`google()`や`jcenter()`を書いても認識されないのでmavenでurlを指定して書く必要があります。
また、この`repositories`ブロックにリポジトリURLを書くだけではplugins構文に書いたplugin idが解決できないことが多くあります。
どうやら、plugin idに指定したプラグインのartifactは`[指定したid]:[指定したid].gradle.plugin:[version]`の形式である必要があるらしく、この形式になっていないものは`resolutionStrategy`ブロックで自ら解決しなくてはいけません。

```groovy
resolutionStrategy {
    eachPlugin {
        switch (requested.id.id) {
            case "com.android.application":
                useModule("com.android.tools.build:gradle:${requested.version}")
                break
```

書き方は難しくなく、`requested.id.id`に`plugins`ブロックで記載したidが入っているので、その値を見て元々`buildscript`ブロックで指定していた`classpath`の値で解決するように指定するだけです。

### ハマリポイント 3: 任意の箇所でapplyしたい

firebaseを使うときには`com.google.gms.google-services`プラグインをbuild.gradleの末尾でapplyする必要があります。
しかし、plugins構文を使うと即時applyされる && pluginsブロックはファイル内に1つという制限があります。

applyのタイミングを任意に変える時はpluginのid指定時に`apply false`を指定することで即時applyを防ぎ、適用したい箇所で改めてapplyを記述することで実現できます。

```kotlin
plugins {

    ...

    id("com.google.gms.google-services") version "3.1.2" apply false
}

...

apply(mapOf("plugin" to "com.google.gms.google-services"))
```


長くなってしまったのでpart 2に続きます(予定)。


## appendix

<blockquote class="twitter-tweet" data-conversation="none" data-cards="hidden" data-partner="tweetdeck"><p lang="ja" dir="ltr">apply {<br>    from &quot;mybuild.gradle&quot;<br>}<br><br>でいけるよとのことでした。若干字が汚く細かい文法がわかりません。。。自分は移行ツールがOSSで出るので完全移行そちらに任せてしまおうと決意しました。</p>&mdash; shiraji (@shiraj_i) <a href="https://twitter.com/shiraj_i/status/926603772368797696?ref_src=twsrc%5Etfw">November 4, 2017</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

```
apply {
  from("${project.rootDir}/app/old_build.gradle")
}
```

という感じで以前のbuild.gradleを読み込んで徐々にkts化していくことができそうです

続きはこちら
[build\.gradle\.ktsへの書き換えとハマリポイント part 2](https://qiita.com/chibatching/items/3b40aea222b2a2f29d4b)