@CacheableTask 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 } } class Utils implements Plugin { final def ARCH_CODES = ["armeabi-v7a": "Arm", "arm64-v8a": "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) } void addTask(String prefix, Closure taskClosure) { forEachBuildVariant { 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 { 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()) } } } } } } void forEachBuildType(Closure callback) { project.android.buildTypes.each { buildType -> callback(buildType.name) } } def getBuildTypesRegExp() { def outList = [] project.android.buildTypes.each { buildType -> outList += buildType.name.capitalize() } return outList } def getArchTypesRegExp() { def outList = [] project.android.productFlavors.each { flavor -> if (flavor.dimension == 'arch') { outList += flavor.name.capitalize() } } return outList } 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="' + arch.uncapitalize() + '"\n' content += "is_debug=${buildType != 'Release'}\n" content += 'ndk="' + project.android.ndkDirectory + '"\n' content += "ndk_api=${project.rootProject.ext.minSdk}\n" return content } def getOutDir(String buildType) { return "${project.buildDir}/gn_out/${buildType.toLowerCase()}" } def getAssetsDir(String buildType) { return "${project.buildDir}/gn_out/${buildType.toLowerCase()}/assets" } def getJniLibsDir(String buildType) { return "${project.buildDir}/gn_out/jniLibs/${buildType.toLowerCase()}" } } 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", "provider_name", "${applicationId}.fileprovider" resValue "string", "app_name", "Kaliber Hello World" resValue "string", "interstitial_ad_unit_id", "" ext { gnTarget = "hello_world" } } demo { dimension 'game' applicationId 'com.kaliber.woom' resValue "string", "provider_name", "${applicationId}.fileprovider" resValue "string", "app_name", "Kaliber Demo" resValue "string", "interstitial_ad_unit_id", "ca-app-pub-1321063817979967/8373182022" ext { gnTarget = "demo" } } arm { dimension 'arch' ndk { abiFilters = ["armeabi-v7a"] } } arm64 { dimension 'arch' ndk { abiFilters = ["arm64-v8a"] } } x86 { dimension 'arch' ndk { abiFilters = ["x86"] } } x64 { 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"] } } } sourceSets { main { java.srcDirs += ['../../../src/engine/platform/java/com/kaliber/base'] utils.forEachBuildType { buildType -> "${buildType}" { assets.srcDirs = [utils.getAssetsDir(buildType)] } } } utils.forEachBuildType { buildType -> "${buildType}" { jniLibs.srcDirs = [utils.getJniLibsDir(buildType)] } } } 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' } utils.addTask('generateGnArgsFor') { String taskName, String buildType, String arch, String game -> task(taskName, type: WriteFileTask) { content = utils.generateGnArgsContent(buildType, arch) target = project.layout.file(provider { new File("${utils.getOutDir(buildType)}/${arch}", 'args.gn') }) } } utils.addTask('runGnFor') { String taskName, String buildType, String arch, String game -> task(taskName, type: Exec) { dependsOn "generateGnArgsFor${game}${arch}${buildType}" executable rootProject.ext.gn args '--fail-on-unused-args', 'gen', "${utils.getOutDir(buildType)}/${arch}" inputs.file(new File("${utils.getOutDir(buildType)}/${arch}", 'args.gn')) outputs.file(new File("${utils.getOutDir(buildType)}/${arch}", 'build.ninja')) } } utils.addTask('runNinjaFor') { String taskName, String buildType, String arch, String game -> task(taskName, type: Exec) { dependsOn "runGnFor${game}${arch}${buildType}" executable rootProject.ext.ninja args '-C', "${utils.getOutDir(buildType)}/${arch}", utils.getGnTargetFor(game) // Always run ninja and let it figure out what needs to be compiled. outputs.upToDateWhen { false } } } utils.addTask('copyAssetsFor') { String taskName, String buildType, String arch, String game -> task(taskName, type: Copy) { dependsOn "runNinjaFor${game}${arch}${buildType}" from "${utils.getOutDir(buildType)}/${arch}/assets" into utils.getAssetsDir(buildType) } } utils.addTask('copyJniLibsFor') { String taskName, String buildType, String arch, String game -> task(taskName, type: Copy) { dependsOn "runNinjaFor${game}${arch}${buildType}" from("${utils.getOutDir(buildType)}/${arch}") { include "lib${utils.getGnTargetFor(game)}.so" rename "lib${utils.getGnTargetFor(game)}.so", "libkaliber.so" } into "${utils.getJniLibsDir(buildType)}/${utils.getAbiCodeFor(arch)}" } } tasks.configureEach { task -> def variantPattern = /(\w+)(${utils.getArchTypesRegExp().join('|')})(${utils.getBuildTypesRegExp().join('|')})/ def match = task.name =~ /^merge/ + variantPattern + /JniLibFolders$/ if (match) { 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 =~ /^merge/ + variantPattern + /Assets$/ if (match) { 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) { 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 } }