apply plugin: 'com.android.application' apply plugin: Utils android { compileSdk rootProject.ext.compileSdk ndkVersion rootProject.ext.ndkVersion defaultConfig { minSdk rootProject.ext.minSdk targetSdk rootProject.ext.targetSdk ndk { abiFilters = [] } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } flavorDimensions 'game', 'arch' productFlavors { helloWorld { dimension 'game' applicationId 'com.kaliber.helloworld' resValue "string", "app_name", "Kaliber Hello World" manifestPlaceholders = [appIcon: "@mipmap/ic_launcher"] ext { gnTarget = "hello_world" } } demo { dimension 'game' applicationId 'com.kaliber.woom' resValue "string", "app_name", "Kaliber Demo" manifestPlaceholders = [appIcon: "@mipmap/ic_launcher"] ext { gnTarget = "demo" } } woom { dimension 'game' applicationId 'com.woom.game' resValue "string", "app_name", "woom" resValue "string", "interstitial_ad_unit_id", "ca-app-pub-1321063817979967/8373182022" resValue "string", "admob_application_id", "ca-app-pub-1321063817979967~1100949243" manifestPlaceholders = [appIcon: "@mipmap/ic_launcher"] ext { gnTarget = "demo" } } arm7 { dimension 'arch' ndk { abiFilters = ["armeabi-v7a"] } } arm8 { dimension 'arch' ndk { abiFilters = ["arm64-v8a"] } } x86 { dimension 'arch' ndk { abiFilters = ["x86"] } } x86_64 { dimension 'arch' ndk { abiFilters = ["x86_64"] } } allArchs { dimension 'arch' ndk { abiFilters = ["armeabi-v7a", "arm64-v8a", "x86_64", "x86"] } } armOnly { dimension 'arch' ndk { abiFilters = ["armeabi-v7a", "arm64-v8a"] } } x86Only { dimension 'arch' ndk { abiFilters = ["x86_64", "x86"] } } // Native library name is same as GN target name. android.productFlavors.each { flavor -> if (flavor.dimension == 'game') { "${flavor.name}" { resValue "string", "lib_name", flavor.ext.gnTarget buildConfigField 'String', 'NATIVE_LIBRARY', "\"${flavor.ext.gnTarget}\"" } } } } sourceSets { main { java.srcDirs += ['../../../src/engine/platform/java/com/kaliber/base'] android.buildTypes.each { buildType -> "${buildType.name}" { assets.srcDirs = ["${utils.getGnOutDir(buildType.name)}/assets"] } } } android.buildTypes.each { buildType -> "${buildType.name}" { jniLibs.srcDirs = ["${utils.getGnOutDir(buildType.name)}/jniLibs"] } } } namespace "com.kaliber.base" } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'com.google.android.gms:play-services-ads:22.0.0' } // // Tasks for GN build // // Generate `args.gn` which is used by GN to take in build arguments. utils.addTask('generateGnArgsFor') { String taskName, String buildType, String arch -> task(taskName, type: WriteFileTask) { content = utils.generateGnArgsContent(buildType, arch) target = project.layout.file(provider { new File("${utils.getGnOutDir(buildType)}/${arch}", 'args.gn') }) } } // Run `gn gen` to generate ninja files. utils.addTask('runGnFor') { String taskName, String buildType, String arch -> task(taskName, type: Exec) { dependsOn "generateGnArgsFor${arch}${buildType}" executable rootProject.ext.gn args '--fail-on-unused-args', 'gen', "${utils.getGnOutDir(buildType)}/${arch}" // Need to run gn only once unless the configuration in `args.gn` changes. inputs.file(new File("${utils.getGnOutDir(buildType)}/${arch}", 'args.gn')) outputs.file(new File("${utils.getGnOutDir(buildType)}/${arch}", 'build.ninja')) } } // Build the native library for the target game using ninja. utils.addGameTask('runNinjaFor') { String taskName, String buildType, String arch, String game -> task(taskName, type: Exec) { dependsOn "runGnFor${arch}${buildType}" executable rootProject.ext.ninja args '-C', "${utils.getGnOutDir(buildType)}/${arch}", "src/${utils.getGnTargetFor(game)}" // Always run ninja and let it figure out what needs to be compiled. outputs.upToDateWhen { false } } } // Assets can be obtained from the output directory of any arch but it would be good to combine them // in a single directory in case we are buildig multi-arch and some build config have different assets. utils.addGameTask('copyAssetsFor') { String taskName, String buildType, String arch, String game -> task(taskName, type: Copy) { dependsOn "runNinjaFor${game}${arch}${buildType}" from "${utils.getGnOutDir(buildType)}/${arch}/assets" into "${utils.getGnOutDir(buildType)}/assets" } } // Copy the native library to a directory denoting its arch code as the Android Gradle plugin expects. utils.addGameTask('copyJniLibsFor') { String taskName, String buildType, String arch, String game -> task(taskName, type: Copy) { dependsOn "runNinjaFor${game}${arch}${buildType}" from("${utils.getGnOutDir(buildType)}/${arch}") { include "lib${utils.getGnTargetFor(game)}.so" } into "${utils.getGnOutDir(buildType)}/jniLibs/${utils.getAbiCodeFor(arch)}" } } tasks.configureEach { task -> def variantPattern = /(\w+)(${utils.getArchTypesRegExp()})(${utils.getBuildTypesRegExp()})/ def match = task.name =~ /^merge/ + variantPattern + /JniLibFolders$/ if (match) { utils.project.android.productFlavors.find { arch -> if (arch.dimension == 'arch' && arch.name.capitalize() == match.group(2)) { // Depends on each arch type for multi-arch build flavors. arch.ndk.abiFilters.each { abi -> task.dependsOn "copyJniLibsFor${match.group(1)}${utils.ARCH_CODES[abi]}${match.group(3)}" } return true } } return } match = task.name =~ /^generate/ + variantPattern + /Assets$/ if (match) { utils.project.android.productFlavors.find { arch -> if (arch.dimension == 'arch' && arch.name.capitalize() == match.group(2)) { // Depends on each arch type for multi-arch build flavors. arch.ndk.abiFilters.each { abi -> task.dependsOn "copyAssetsFor${match.group(1)}${utils.ARCH_CODES[abi]}${match.group(3)}" } return true } } return } match = task.name =~ /^lintVitalAnalyze/ + variantPattern + /$/ if (match) { utils.project.android.productFlavors.find { arch -> if (arch.dimension == 'arch' && arch.name.capitalize() == match.group(2)) { // Depends on each arch type for multi-arch build flavors. arch.ndk.abiFilters.each { abi -> task.dependsOn "copyAssetsFor${match.group(1)}${utils.ARCH_CODES[abi]}${match.group(3)}" } return true } } return } } // // Utils plugin // class Utils implements Plugin { final def ARCH_CODES = ["armeabi-v7a": "Arm7", "arm64-v8a": "Arm8", "x86_64": "X86_64", "x86": "X86"].asImmutable() final def GN_CPU_CODES = ["Arm7": "arm", "Arm8": "arm64", "X86_64": "x64", "X86": "x86"].asImmutable() def project @Inject Utils(Project project) { this.project = project } void apply(Project project) { project.extensions.create('utils', Utils) } // Add a task for archs and buildTypes variants. void addTask(String prefix, Closure taskClosure) { forEachBuildVariant { String arch, String buildType -> def taskName = "${prefix}${arch}${buildType}" taskClosure(taskName, buildType, arch) } } // Add a task for games, archs and buildTypes variants. void addGameTask(String prefix, Closure taskClosure) { forEachGameBuildVariant { String game, String arch, String buildType -> def taskName = "${prefix}${game}${arch}${buildType}" taskClosure(taskName, buildType, arch, game) } } void forEachBuildVariant(Closure callback) { project.android.productFlavors.each { arch -> // Only need to add tasks for arch types which maps to a single ABI if (arch.dimension == 'arch' && arch.ndk.abiFilters.size() == 1) { project.android.buildTypes.each { buildType -> callback(arch.name.capitalize(), buildType.name.capitalize()) } } } } void forEachGameBuildVariant(Closure callback) { project.android.productFlavors.each { game -> if (game.dimension == 'game') { project.android.productFlavors.each { arch -> // Only need to add tasks for arch types which maps to a single ABI if (arch.dimension == 'arch' && arch.ndk.abiFilters.size() == 1) { project.android.buildTypes.each { buildType -> callback(game.name.capitalize(), arch.name.capitalize(), buildType.name.capitalize()) } } } } } } def getBuildTypesRegExp() { def outList = [] project.android.buildTypes.each { buildType -> outList += buildType.name.capitalize() } return outList.join('|') } def getArchTypesRegExp() { def outList = [] project.android.productFlavors.each { flavor -> if (flavor.dimension == 'arch') { outList += flavor.name.capitalize() } } return outList.join('|') } def getAbiCodeFor(String arch) { def outStr = '' project.android.productFlavors.find { flavor -> if (flavor.name.capitalize() == arch) { outStr = flavor.ndk.abiFilters.first() return true } } return outStr } def getGnTargetFor(String game) { def outStr = '' project.android.productFlavors.find { flavor -> if (flavor.dimension == 'game' && flavor.name.capitalize() == game) { outStr = flavor.ext.gnTarget return true } } return outStr } def generateGnArgsContent(String buildType, String arch) { def content = 'target_os="android"\n' content += 'target_cpu="' + GN_CPU_CODES[arch] + '"\n' content += "is_debug=${buildType != 'Release'}\n" content += 'ndk="' + project.android.ndkDirectory + '"\n' content += "ndk_api=${project.rootProject.ext.minSdk}\n" return content } def getGnOutDir(String buildType) { return "${project.buildDir}/gn_out/${buildType.toLowerCase()}" } } abstract class WriteFileTask extends DefaultTask { @Input abstract Property getContent() @OutputFile abstract RegularFileProperty getTarget() @TaskAction void run() { def file = target.get().asFile file.parentFile.mkdirs() def text = content.get() if (!file.exists() || text != file.text) file.text = text } }