Edited at

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

More than 3 years have passed since last update.


前置き

2015年にEclipseのサポートが終了するとのことで

Android Studioに統合されているビルドシステムGradleでビルドしてみました。

Google、Android Eclipseツールのサポートを終了

An update on Eclipse Android Developer Tools

ビルド環境を変えてもお金が発生するわけじゃないんだよなーと思いつつも、

とりあえず古い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


下準備


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ファイルの出力先なのでシンボリックリンクよりフォルダ作成した方がいい)

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

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 を認識させるメモ

ここではファイル名を「build_common.gradle」としています。

このファイル名は以下のパスの設定にもでてくるので注意。


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内でプロパティとして参照することができる。

パスは絶対パスを設定して確認しました。

相対パスは確認していません。


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を編集する。


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の編集。

このプロパティリストに設定すると全プロジェクトに反映されるので注意。


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からパスを読み込んでプロジェクトを設定する。


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ファイルを作成する。


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ファイルを編集します。

必要な設定は一部です。

同名のファイルが他にもあるので注意!


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から環境変数を読み込む処理。


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)

タスクの設定に誤りがないか確認する。

$ ./gradlew -m build

エラーがあればエラーをつぶす。

エラーがなければリリースビルドを実行する。

(下記コマンドはcleanとcheckを合わせて実行しています)

$ ./gradlew clean check assembleRelease

下のような署名用のパスワード入力ダイアログまたはターミナル内で問い合わせがあれば成功。

フルウィンドウでなくデスクトップ画面上に表示されているので注意!

スクリーンショット 2015-12-30 21.59.02.png

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

./gradlew clean check assembleRelease 2>&1 | tee ./build.log


確認と便利コマンド


apkファイルの確認

proj.androidstudio/[アプリ名]/app/build/outputs/apkフォルダにapkファイルが出力されていること。


署名の確認

$ keytool -list -printcert -jarfile [ファイル名].apk


zipAlignの確認

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

BAD列が出力されているときはzipAlignが実行されていない。

build.gradleの確認を。


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

./gradlew --rerun-tasks [タスク名]

例)
./gradlew --rerun-tasks assembleRelease


タスクの一覧を表示する

./gradlew tasks


依存関係の調査

./gradlew app:dependencies


あとがき

これまでの設定とおりGradleは設定内容が複雑になりがちな気がしています。

Googleは2016年末に新しいビルドシステムBazelをリリース予定 :innocent:

Bazel x Android NDK でビルドする (ついでにiOSも)

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

Experimental Plugin User Guide

ビルドシステムは空気みたい存在になりつつあるので、

柔軟性がなくても制限が強くて直感的に編集できるものがいいですよね!

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