Cocos2dx 2.2.6のAndroidアプリをAndroid StudioのGradleでビルドする (Mac版)

#前置き
2015年にEclipseのサポートが終了するとのことで
Android Studioに統合されているビルドシステムGradleでビルドしてみました。

[Google、Android Eclipseツールのサポートを終了](http://www.infoq.com/jp/news/2015/07/google-android-eclipse)
[An update on Eclipse Android Developer Tools](http://android-developers.blogspot.jp/2015/06/an-update-on-eclipse-android-developer.html)

ビルド環境を変えてもお金が発生するわけじゃないんだよなーと思いつつも、
とりあえず古いcocos2dx 2.2.6のプロジェクトをGradleでビルドできるようにしました。

ちなみに、Android Studioってなに？Gradleってなに？Groovyってなに？って状態から作ったものなので、ところどころおかしな点があるかもしれません。

残念ながら質問は受け付けてないので悪しからず。。。 :sweat: 


#前提条件
テスト環境:
- Android Studio 1.5.1
- Gradle 2.8
- Android Build Tools ver 23.0.2
- Android gradle plugin v1.5 (com.android.tools.build:gradle:1.5.0)

Android Studioでもビルドできるようにしているが
メインはターミナルからコマンドでビルドをすることを想定してます。

Android PluginのDSLは下位互換性がないことが多く、
Gradleのバージョンによっては動作しないことも考えられます。

Android pluginのNDK DSLを使用してビルドすることを試みてみました。
gradleのc++ DSLがいまだ開発中（アルファ版くらいかな!?）だったり
Androud NDK gradle pluginがまだ実験中ともあって、結局ndk-buildを使うことにしました。

[CppSourceSet - Gradle Document](https://docs.gradle.org/current/dsl/org.gradle.language.cpp.CppSourceSet.html)


#下準備
##Android Studioで新規プロジェクトを作成しよう
cocos2dxで作られたproj.androidと同じ階層に
proj.androidstudioというフォルダを作って、そこにプロジェクトを作成する。
(以下、Android Studioのルートフォルダをproj.androidstudioとします)
(特にどこにフォルダを作成しても問題ありません)

プロジェクトはActivtyを作らない空のプロジェクトを作成します。
(「Add No Activity」テンプレートを選択)


##ファイルのコピーやらリンクやら

- proj.androidstudio/[アプリ名]/app/src/main/AndroidManifest.xmlをオリジナルのもので上書きする。
- proj.androidstudio/[アプリ名]/app/libsフォルダを作成してjarファイルをコピーする。(*.soファイルの出力先なのでシンボリックリンクよりフォルダ作成した方がいい）


以下のようにシンボリックリンクを設定する。

```shell-session:
ln -s src dst
```

| src | dst |
|:------------|:------------|:------------|
| proj.android/jni | proj.androidstudio/[アプリ名]/app/jni |
| proj.android/res | proj.androidstudio/[アプリ名]/app/res |
| proj.android/../Resources | proj.androidstudio/[アプリ名]/app/assets |
| proj.android/src/[トップディレクトリ] | proj.androidstudio/[アプリ名]/app/src/[srcと同じフォルダ名] |


##共有build_common.gradleファイルの作成
ndk-buildと連携するためのタスクなど共通して使用する定義を作成する。
他のプロジェクトと共有して使用できるので、ファイルの場所はプロジェクトフォルダ配下より共有できる場所に置いておくと便利。

ndk-buildとの連携は以下のサイトを参考にしています。
[AndroidStudio 1.5 でNDK を認識させるメモ](http://d.hatena.ne.jp/kimukou_26/20151111/p1)

ここではファイル名を「build_common.gradle」としています。
このファイル名は以下のパスの設定にもでてくるので注意。

```groovy:build_common.gradle
//ndk レガシータスクを無効にする
tasks.whenTaskAdded { task ->
    if (task.name.indexOf("Ndk")!=-1) {
        task.enabled = false
    }
}

ext {
    ndkHome = android.ndkDirectory ==null ? "":android.ndkDirectory.getAbsolutePath()
    cpuNum = Runtime.getRuntime().availableProcessors()

    requiredPropertyNames = ['MY_COCOS2DX_CLASSES_LOC', 'COCOS2DX_LOC']
}

import org.apache.tools.ant.taskdefs.condition.Os

task ndkBuildDebug() {

    description "Execute ndk-build debug mode."
    onlyIf { file('jni').exists() }

    inputs.files getNdkSourceFileTree()

    // MEMO: ndk-buildした結果、どんな*.soファイルができるかはgradleの中からは知る由もない
    //       そのため常に出力ファイルは更新されていないものとする
    outputs.upToDateWhen { true }

    def parentTask = project.tasks.find {
        ":app:assembleDebug".equalsIgnoreCase(it.path)
    }
    if(parentTask!=null) {
        parentTask.dependsOn ndkBuildDebug
    }

    doLast {
        println "--------------- NDK build [DEBUG] ---------------"
        println "project.projectDir:" + project.projectDir

        exec {
            environment getNdkEnvironments()
            println "environment:" + environment

            if (Os.isFamily(Os.FAMILY_WINDOWS)) {
                commandLine "${ndkHome}/ndk-build.cmd", "NDK_DEBUG=1", "-j", cpuNum, "NDK_LOG=1", "-C", project.projectDir, getCocos2dxModulePath()
            } else {
                commandLine "${ndkHome}/ndk-build", "NDK_DEBUG=1", "-j", cpuNum, "NDK_LOG=1", "-C", project.projectDir, getCocos2dxModulePath()
            }
        }
    }
}

task ndkBuildRelease() {

    description "Execute ndk-build release mode."
    onlyIf { file('jni').exists() }

    inputs.files getNdkSourceFileTree()

    // MEMO: ndk-buildした結果、どんな*.soファイルができるかはgradleの中からは知る由もない
    //       そのため常に出力ファイルは更新されていないものとする
    outputs.upToDateWhen { true }

    def parentTask = project.tasks.find {
        ":app:assembleRelease".equalsIgnoreCase(it.path)
    }
    if(parentTask!=null) {
        parentTask.dependsOn ndkBuildRelease
    }

    doLast {
        println "--------------- NDK build [RELEASE] ---------------"
        println "project.projectDir:" + project.projectDir

        exec {
            environment getNdkEnvironments()
            println "environment:" + environment

            if (Os.isFamily(Os.FAMILY_WINDOWS)) {
                commandLine "${ndkHome}/ndk-build.cmd", "-j", cpuNum, "NDK_LOG=1", "-C", project.projectDir, getCocos2dxModulePath()
            } else {
                commandLine "${ndkHome}/ndk-build", "-j", cpuNum, "NDK_LOG=1", "-C", project.projectDir, getCocos2dxModulePath()
            }
        }
    }
}


clean.dependsOn 'ndkClean'

task ndkClean(type: Delete) {

    description "Execute ndk-build clean, and delete obj files."
    onlyIf { file('jni').exists() }
    checkRequiredPropertyNames()

    inputs.files getNdkSourceFileTree()

    // MEMO: ndk-buildした結果、どんな*.soファイルができるかはgradleの中からは知る由もない
    //       そのため常に出力ファイルは更新されていないものとする
    outputs.upToDateWhen { true }

    doLast {
        println "--------------- NDK clean ---------------"

        exec {
            environment getNdkEnvironments()
            println "environment:" + environment

            workingDir project.projectDir
            ignoreExitValue true

            if (Os.isFamily(Os.FAMILY_WINDOWS)) {
                commandLine "${ndkHome}/ndk-build.cmd", 'clean'
            } else {
                commandLine "${ndkHome}/ndk-build", "-j", cpuNum, "clean", "NDK_LOG=1", "-C", project.projectDir, getCocos2dxModulePath()
            }

            File file = new File(project.projectDir, 'obj')
            if(file.exists()) {
                String objDirPath = file.getAbsolutePath()
                println "Delete obj directory:" + objDirPath
                delete objDirPath
            }
        }
    }
}

/**
 * このプロジェクトを実行するのに必要なプロパティをチェックする
 */
def checkRequiredPropertyNames() {
    requiredPropertyNames.each() { name ->
        if(!project.hasProperty(name)) {
            throw new StopExecutionException("Not found $name of property in gradle.properties.")
        }
        println "project.property[$name]:" + project.property(name)
    }
}

/**
 * NDKでビルドするファイルセットを取得する関数
 * この関数内でファイルを設定することによって監視対象となり
 * いずれかのファイルが変更された場合タスクが実行されるようになる
 */
def getNdkSourceFileTree() {

    // MEMO: 取り込む拡張子の定義
    def includeList = ["**/*.h", "**/*.hpp", "**/*.c", "**/*.cc", "**/*.cpp", "**/*.mk"]
    def list = []

    // MEMO: 監視対象のコードを追加する
    requiredPropertyNames.each() { name ->
        list.add(file(project.property(name)))
    }

    FileTree fileTrees = fileTree(dir:new File(project.projectDir, "jni"), include:includeList)

    list.each {
        fileTrees += fileTree(dir:it, include:includeList)
    }

    return fileTrees
}

/**
 * Android.mk, Application.mkファイルに値を渡す環境変数を設定する
 *
 * env['環境変数名'] = 値
 */
def getNdkEnvironments() {
    def env = new HashMap<String, Object>();
    env['COCOS2DX_CLASSES_LOC'] = file(MY_COCOS2DX_CLASSES_LOC)
    return env
}

def getCocos2dxModulePath() {
    String path = file(COCOS2DX_LOC).getAbsolutePath()
    String modulePath = "NDK_MODULE_PATH=" + path + ":" + path + "/cocos2dx/platform/third_party/android/prebuilt"
    return modulePath
}

//-----------------------------------------------------------------------------
// 署名のパスワード入力処理
//-----------------------------------------------------------------------------
import groovy.swing.SwingBuilder

// MEMO: https://coderwall.com/p/zrdsmq/signing-configs-with-gradle-android
//       上記サイトではgradle.taskGraph.whenReady()で実行していたが、
//       このタイミングで実行しても処理が反映されなかったので以下のように変更。

gradle.afterProject { proj ->
    println "--------------- gradle.afterProject ---------------"
    println "gradle.startParameter.taskNames:" + gradle.startParameter.taskNames

    boolean isFind = false
    def taskNames = gradle.startParameter.taskNames.findAll() { it.indexOf('build')!=-1 }
    taskNames += gradle.startParameter.taskNames.findAll() { it.indexOf('assembleRelease')!=-1 }

    if(taskNames!=null && taskNames.size()>0) isFind = true

    if(isFind) {
        tasks.find() { task ->
            if(task.path==':app:assembleRelease')
            {
                if(proj.android.signingConfigs.release.storePassword!=null &&
                   proj.android.signingConfigs.release.storePassword.length()>0) {
                    println "Already set storePassword at " + task.path
                    return false;
                }

                if(proj.android.signingConfigs.release.keyPassword!=null &&
                   proj.android.signingConfigs.release.keyPassword.length()>0) {
                    println "Already set keyPassword at " + task.path
                    return false;
                }

                def storePass = ''
                def keyPass = ''
                Console console = System.console()
                println "Match ${task.path}. console:" + console

                if(console == null) {
                    new SwingBuilder().edt {
                        dialog(modal: true,
                                title: 'Enter password',
                                alwaysOnTop: true,
                                resizable: false,
                                locationRelativeTo: null,
                                pack: true,
                                show: true) {
                            vbox { // Put everything below each other
                                label(text: "Please enter store passphrase:")
                                def input1 = passwordField()
                                label(text: "Please enter key passphrase:")
                                def input2 = passwordField()
                                button(defaultButton: true,
                                        text: 'OK',
                                        actionPerformed: {
                                            storePass = input1.password;
                                            keyPass = input2.password;
                                            dispose();
                                        })
                            }
                        }
                    }
                } else {
                    storePass = console.readPassword("\nPlease enter store passphrase: ")
                    keyPass = console.readPassword("\nPlease enter key passphrase: ")
                }

                if(storePass.size() <= 0 || keyPass.size() <= 0) {
                    throw new InvalidUserDataException("You must enter the passwords to proceed.")
                }

                storePass = new String(storePass)
                keyPass = new String(keyPass)

                proj.android.signingConfigs.release.storePassword = storePass
                proj.android.signingConfigs.release.keyPassword = keyPass
                return true;
            }
        }
    }
}

```
注) パスワード入力処理は「build」「assembleRelease」タスクのみで実行されるようになっています。「assemble」では動きません。

##gradle.propertiesの設定
gradle.propertiesに変数を設定するとbuild.gradle内でプロパティとして参照することができる。
パスは絶対パスを設定して確認しました。
相対パスは確認していません。


```properties:proj.androidstudio/[アプリ名]/gradle.properties
COCOS2DX_LOC=[cocos2dxのルートフォルダ]
MY_COCOS2DX_CLASSES_LOC=[proj.android/../Classesのパス]
MY_BUILD_COMMON_GRADLE=[先ほど設定したbuild_common.gradleのパス(ファイル名も含む)]
```

##local.propertiesの設定
proj.androidstudio/[アプリ名]/にあるlocal.propertiesを編集する。

```properties:proj.androidstudio/[アプリ名]/local.properties
ndk.dir=[Android NDKのパス]
sdk.dir=[Android SDKのパス]

COCOS2DX_JAVA_LOC=[cocos2dxのルートフォルダ]/cocos2dx/platform/android/java

# 例) 他社SDKをEclipseにインポートして使用しているとき以下のように設定する
MY_LOBI_CORE_LIBRARY_LOC=[他社SDKのEclipseプロジェクトのパス]
MY_LOBI_RANKING_LIBRARY_LOC=[他社SDKのEclipseプロジェクトのパス]
```

##ユーザフォルダにあるgradle.propertiesの設定
~/.gradle/gradle.propertiesの編集。
このプロパティリストに設定すると全プロジェクトに反映されるので注意。

```properties:gradle.properties
# gradleをdaemonモードで実行する。
# 実行速度が速くなるのでオススメ
org.gradle.daemon=true

MY_KEYSTORE_DEBUG_LOC=[ユーザフォルダ]/.android/debug.keystore
MY_KEYSTORE_RELEASE_LOC=[署名用のキーファイル 例)android_rsa]
MY_KEYSTORE_RELEASE_ALIAS=[キーストアのエイリアス名]
```



##settings.gradleの設定
local.propertiesからパスを読み込んでプロジェクトを設定する。

```groovy:proj.androidstudio/[アプリ名]/settings.gradle
include ':app'
include ':cocos2dx'
include ':LobiCore'
include ':LobiRanking'

def getLoaclProperties(String defineName) {
    Properties properties = new Properties()
    properties.load(new File(rootDir.absolutePath + "/local.properties").newDataInputStream())
    def defineValue = properties.getProperty(defineName, null)
    if (defineValue == null) {
        throw new GradleException(
                "Module location not found. Define location with '$defineName' in the local.properties file!")
    }
    println "defineName:" + defineName + ", defineValue:" + defineValue + " at local.properties."
    return defineValue
}

project(':cocos2dx').projectDir = new File(getLoaclProperties('COCOS2DX_JAVA_LOC'))

// 例) 他社SDKをEclipseにインポートして使用しているとき以下のように設定する
project(':LobiCore').projectDir = new File(getLoaclProperties('MY_LOBI_CORE_LIBRARY_LOC'))
project(':LobiRanking').projectDir = new File(getLoaclProperties('MY_LOBI_RANKING_LIBRARY_LOC'))
```


##build.gradleの設定
build.gradleはライブラリ側とアプリケーション側、両方必要になります。

###ライブラリ側のbuild.gradleを設定
cocos2dxのAndroidライブラリをビルドする。

[cocos2dxルートフォルダ]/cocos2dx/platform/android/java

上記パスにbuild.gradleファイルを作成する。

```groovy:build.gradle
// MEMO: ライブラリ用のプラグイン
apply plugin: 'com.android.library'

android {
    compileSdkVersion 19
    buildToolsVersion "23.0.2"

    defaultConfig {
        minSdkVersion 8
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_7
        targetCompatibility JavaVersion.VERSION_1_7
    }

    lintOptions {
        abortOnError false
    }

    buildTypes {
        debug {
            debuggable true
        }
        release {
            debuggable false
            jniDebuggable false
            zipAlignEnabled true
            minifyEnabled false
            proguardFile getDefaultProguardFile('proguard-android.txt')
        }
    }

    sourceSets {
          main {
               manifest.srcFile 'AndroidManifest.xml'
               java {
                   srcDirs = ['src']
                   includes = ['**/*.java']
               }
//               resources.srcDirs = ['src']
//               aidl.srcDirs = ['src']
//               renderscript.srcDirs = ['src']
               res.srcDirs = ['res']
               assets.srcDirs = ['assets']
               jniLibs.srcDirs = ['libs']
          }
    }
}

dependencies {
}
```

Eclipseにインポートしていた他社SDKに対しても上記build.gradleを作成する。
proj.androidstudio/[アプリ名]/settings.gradleに対してもパスを設定することも忘れずに。


###アプリケーション側のbuild.gradleを設定
proj.androidstudio/[アプリ名]/app配下にあるbuild.gradleファイルを編集します。
必要な設定は一部です。
同名のファイルが他にもあるので注意！

```groovy:proj.androidstudio/[アプリ名]/app/build.gradle
apply plugin: 'com.android.application'

// ↓ ここから
def checkRequiredPropertyNames() {
    def requiredPropertyNames = ['MY_BUILD_COMMON_GRADLE', 'MY_KEYSTORE_DEBUG_LOC', 'MY_KEYSTORE_RELEASE_LOC', 'MY_KEYSTORE_RELEASE_ALIAS']

    requiredPropertyNames.each() { name ->
        if(!project.hasProperty(name)) {
            throw new StopExecutionException("Not found '$name' of property in gradle.properties.")
        }
    }
}
checkRequiredPropertyNames()

apply from:file(MY_BUILD_COMMON_GRADLE)
// ↑ ここまでの設定が必要です。下にもまだあります。

repositories {
}

android {
    compileSdkVersion 19

    // MEMO: android/sdk/build-toolsの下にあるバージョン
    buildToolsVersion "23.0.2"

    defaultConfig {
        applicationId "[アプリケーションID]"
        minSdkVersion 10
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"

        // apkファイルのベース名
        project.ext.set("archivesBaseName", "[apkのベース名]")
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_7
        targetCompatibility JavaVersion.VERSION_1_7
    }

    lintOptions {
        abortOnError false
        disable 'MissingTranslation' // lintの警告の抑制
    }

// ↓ ここから (必要なのはsigningConfigs処理の部分です)
    // 署名設定の読み込み
    signingConfigs {
        debug {
            // MEMO: これは設定しないでもデフォルトで設定されているらしい
            storeFile file(MY_KEYSTORE_DEBUG_LOC)
            storePassword "android"
            keyAlias "androiddebugkey"
            keyPassword "android"
        }
        release {
            storeFile file(MY_KEYSTORE_RELEASE_LOC)
            keyAlias MY_KEYSTORE_RELEASE_ALIAS
        }
    }

    buildTypes {
        debug {
            debuggable true
            jniDebuggable true
            zipAlignEnabled false
            signingConfig signingConfigs.debug
            minifyEnabled false
        }
        release {
            debuggable false
            jniDebuggable false
            zipAlignEnabled true
            signingConfig signingConfigs.release

            // MEMO: Google Play Servicesライブラリを使用していると
            //       Proguardを有効にするとDex処理でSimExceptionを発生させるためProguardは使用できない
            //       https://code.google.com/p/android/issues/detail?id=187483
            minifyEnabled false
            proguardFile getDefaultProguardFile('proguard-android.txt')
            proguardFile file('proguard-rules.pro')
        }
    }
// ↑ ここまでの設定が必要です。

    sourceSets {
        main {
            java {
                srcDirs = ['src']
                includes = ['**/*.java']
            }
//            resources.srcDirs = ['src']
//            aidl.srcDirs = ['src']
//            renderscript.srcDirs = ['src']
            res.srcDirs = ['res']
            assets.srcDirs = ['assets']

            jni.srcDirs = ['jni']
            jniLibs {
                srcDirs = ['libs']
                includes = ['**/*.so']
            }
        }
    }

    packagingOptions {
        exclude 'META-INF/DEPENDENCIES'
        exclude 'META-INF/INDEX.LIST'
        exclude 'META-INF/LICENSE'
        exclude 'META-INF/NOTICE'
    }
}

// MEMO: *が使用できない
configurations.compile.exclude module:'support-v4'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])

    compile 'com.android.support:support-v4:+'
//    compile 'com.android.support:appcompat-v7:23.1.1'
}
```

##Android.mkの設定例
build_common.gradleファイルで設定した環境変数をAndroid.mkで読み込むことができる。
下記はAndroid.mkから環境変数を読み込む処理。

```mf:Android.mk
$(info COCOS2DX_CLASSES_LOC:$(COCOS2DX_CLASSES_LOC))

ifeq ($(MY_COCOS2DX_CLASSES_LOC),)
# 環境変数が設定されていないときの処理
else
    LOCAL_SRC_FILES += $(shell find $(MY_COCOS2DX_CLASSES_LOC) -name "*.c")
    LOCAL_SRC_FILES += $(shell find $(MY_COCOS2DX_CLASSES_LOC) -name "*.cpp")
endif

```

#ビルドを実行する
##Android StudioからGradle Syncを実行する
設定しているライブラリ等のダウンロードをするためAndroid StudioでGradle Syncを実行する。
(コマンドでもライブラリをダウンロードするような気がするけど...)

Tools -> Android -> Sync Project with Gradle Files


##ターミナルから実行
まずはドライランを実行する。(引数 -m)
タスクの設定に誤りがないか確認する。

```shell-session:
$ ./gradlew -m build
```

エラーがあればエラーをつぶす。
エラーがなければリリースビルドを実行する。
(下記コマンドはcleanとcheckを合わせて実行しています)

```shell-session:
$ ./gradlew clean check assembleRelease
```

下のような署名用のパスワード入力ダイアログまたはターミナル内で問い合わせがあれば成功。
フルウィンドウでなくデスクトップ画面上に表示されているので注意！

![スクリーンショット 2015-12-30 21.59.02.png](https://qiita-image-store.s3.amazonaws.com/0/36154/20310806-d5eb-6f5a-9851-6f00e3c0b7d0.png)

ログを取りながらビルドするとき

```shell-session:
./gradlew clean check assembleRelease 2>&1 | tee ./build.log
```


#確認と便利コマンド
##apkファイルの確認
proj.androidstudio/[アプリ名]/app/build/outputs/apkフォルダにapkファイルが出力されていること。

###署名の確認

```shell-session:
$ keytool -list -printcert -jarfile [ファイル名].apk
```

###zipAlignの確認
```shell-session:
$ [Android SDKのルートパス]/build-tools/[ビルドツールバージョン]/zipalign -c -v 4 [ファイル名].apk | grep BAD
```

BAD列が出力されているときはzipAlignが実行されていない。
build.gradleの確認を。

##gradleのタスクを強制実行させる

```shell-session:
./gradlew --rerun-tasks [タスク名]

例)
./gradlew --rerun-tasks assembleRelease
```

##タスクの一覧を表示する

```shell-session:
./gradlew tasks
```

##依存関係の調査
```shell-session:
./gradlew app:dependencies
```


#あとがき
これまでの設定とおりGradleは設定内容が複雑になりがちな気がしています。
Googleは2016年末に新しいビルドシステムBazelをリリース予定 :innocent: 

[Bazel x Android NDK でビルドする (ついでにiOSも)](http://qiita.com/smith/items/897d977a06d0103f40e0)

Googleが最近だしているNDK用のpluginは未だ実験中 :sob: 

[Experimental Plugin User Guide](http://tools.android.com/tech-docs/new-build-system/gradle-experimental)

ビルドシステムは空気みたい存在になりつつあるので、
柔軟性がなくても制限が強くて直感的に編集できるものがいいですよね！

調査と実験の時間が結構かかっちゃったから、お金ちょーだい！ :smiling_imp: