I'm trying to integrate the conan package manager to install C++ dependencies for my Android app. I first followed the official Conan tutorial and it works. However, the task from the official example always runs and is never up-to-date. This is problematic as every time this task runs, it regenerates the cmake dependency files, making cmake rerun and all code needs to be recompiled. This significantly increases the edit-build-debug cycle.
Therefore, I'm trying to make a more complex gradle task that will know when the conan installation is up-to-date and thus avoid re-running cmake every time I run the app.
This is what I came up with:
apply plugin: 'com.android.application'
// https://stackoverflow.com/a/69099942/213057
tasks.register("prepareKotlinBuildScriptModel"){}
abstract class ConanInstallTask extends DefaultTask {
@Internal
String buildType;
@Internal
String abi;
@TaskAction
void conanInstall() {
def buildDir = new File("app")
buildDir.mkdirs()
def cmd = ['conan', 'install', '/path/to/conanfile/folder/', '-of', '.',
"--profile:host", "android-ndk-r25c-" + abi,
"--profile:build", "macos-xcode-universal-clang-14.0.3",
"--settings", "build_type=" + buildType,
"--build", "missing",
"-c", "tools.cmake.cmake_layout:build_folder_vars=['settings.arch']",
"-c", "tools.cmake.cmaketoolchain:generator=Ninja"]
print(">> ${cmd} \n")
def sout = new StringBuilder(), serr = new StringBuilder()
def proc = cmd.execute(null, buildDir)
proc.consumeProcessOutput(sout, serr)
proc.waitFor()
println "$sout $serr"
if (proc.exitValue() != 0) {
throw new Exception("out> $sout err> $serr" + "\nCommand: ${cmd}")
}
}
}
android {
// the usual cmake-based android NDK project
}
dependencies {
// also as usual
}
android.applicationVariants.all { variant ->
def buildType = variant.buildType.name
android.defaultConfig.externalNativeBuild.cmake.abiFilters.each { abi ->
def conanInstallTask = project.tasks.create( "conanInstall${abi.capitalize()}${buildType.capitalize()}", ConanInstallTask)
conanInstallTask.buildType = buildType.capitalize()
conanInstallTask.abi = abi
conanInstallTask.inputs.file '/path/to/conanfile/folder/conanfile.py'
// note: https://discuss.gradle.org/t/unexpected-up-to-date-task-while-output-file-is-missing/5216/2
conanInstallTask.outputs.upToDateWhen {
def arch = {
switch( abi ) {
case 'arm64-v8a': return 'armv8'
case 'armeabi-v7a': return 'armv7'
default: return abi
}
}()
file( "build/${arch}/${buildType.capitalize()}/generators/conan_toolchain.cmake" ).exists()
}
// how to make configureCMake${buildType}[${abi}] depend on conanInstallTask?
variant.externalNativeBuildProviders[0].get().dependsOn conanInstallTask
// needed for Android Studio code model to be aware of conan
project.tasks.findByName('prepareKotlinBuildScriptModel').dependsOn( conanInstallTask )
}
}
The snippet above works correctly until I click clean
in Android Studio. The next build then fails because configureCMakeDebug[arm64-v8a]
, and similar, fail because they can't find files that conan install task should produce. The conan install tasks are executed, but in parallel or after configureCMake...
tasks.
How to make configureCMake...
tasks depend on my newly added task using Android Gradle Plugin v8's API?
As you can see from the snippet above, variant.externalNativeBuildProviders[0].get()
does not refer to the cmake configure task - it refers to the actual code build task, which happens "too late".
I've also tried finding tasks by name, but this doesn't work (see below):
// doesn't work - NullPointerException on gradle sync
project.tasks.findByName(":app:configureCMake${buildType.capitalize()}[${abi}]").dependsOn( conanInstallTask )
// also doesn't work, gradle sync passes, but throws NPE on app run (app build, however, does work)
project.tasks.findByName("configureCMake${buildType.capitalize()}[${abi}]").dependsOn( conanInstallTask )
// also doesn't work, gradle sync passes, but throws NPE on app run (app build, however, does work)
project.tasks.findByPath(":app:configureCMake${buildType.capitalize()}[${abi}]").dependsOn( conanInstallTask)
EDIT:
for some weird reason, this appears to be working correctly, although I don't have any explanation why:
def cmakeConfigureTask = project.tasks.findByPath(":app:configureCMake${buildType.capitalize()}[${abi}]")
// or def cmakeConfigureTask = project.tasks.findByName("configureCMake${buildType.capitalize()}[${abi}]")
if ( cmakeConfigureTask != null ) {
cmakeConfigureTask.dependsOn(conanInstallTask)
}
It appears that :app:configureCMake...
sometimes exists, and sometimes doesn't ♂️