What is the actual right way to use tasks?

I’m rather new to gradle and i must admit that the syntax is far a way from self-explaining.
I made a copy-task as shown below and i ask me now is this the right way to do?

Or if this is an obsolete way because the most code-snippets uses another way.
(and what is the main difference to them?)

Thx for any further explanatons.

 android.applicationVariants.all { variant ->
    variant.outputs.all { output ->
       def outputFile = output.outputFile
       if (outputFile != null && outputFile.name.endsWith('.apk')) {
          def assembleTaskName = variant.getAssembleProvider().name
          def mustRunAfterTask = "create${variant.name.capitalize()}ApkListingFileRedirect"
          def newTargetName = theAppName + "-v" + variant.versionName + "-" + variant.name + ".apk"
          def copyTask = project.tasks.create("copyAndRenameOutputAPKFile$assembleTaskName", Copy)
          copyTask.from(outputFile.parent) {
              include outputFileName
          }
          copyTask.into(rootProjDir)
          copyTask.rename outputFileName , newTargetName
          copyTask.dependsOn assembleTaskName
          copyTask.mustRunAfter mustRunAfterTask
          copyTask.doLast() {
              println "Copied ${outputFileName} to ${newTargetName}"
          }
          tasks[assembleTaskName].finalizedBy = [copyTask]
       }
    }
}

Nobody has a proposal?

That’s more an Android question than a Gradle question.
The Android Gradle Plugin is a completely separate project.

Regarding “self-explanatoryness”, I greatly recommend using the Kotlin DSL instead.
You immediately get type-safe build scripts, proper helpful error messages if you mess up the syntax, and amazingly better IDE support.

I’m no Android developer, so I have no idea whether there is a more appropriate way to do what you want to, but purley from the snippet you showed, here a version that at least removes some bad-practices and problematic parts:

android.applicationVariants.configureEach { variant ->
    variant.outputs.configureEach { output ->
        def outputFile = output.outputFile
        if (outputFile != null && outputFile.name.endsWith('.apk')) {
            def assembleTask = variant.assembleProvider
            def mustRunAfterTaskName = "create${variant.name.capitalize()}ApkListingFileRedirect"
            def newTargetName = "${theAppName}-v${variant.versionName}-${variant.name}.apk"
            def copyTask = project.tasks.register("copyAndRenameOutputAPKFile${assembleTask.name.capitalize()}") {
                mustRunAfter(mustRunAfterTaskName)
                intpus.files(assembleTask)
                outputs.file(rootProject.file(newTargetName))
                doLast {
                    copy {
                        from(assembleTask)
                        into(rootDir)
                        rename(outputFile.name, newTargetName)
                    }
                    println("Copied ${outputFile.name} to ${newTargetName}")
                }
            }
            assembleTask.configure { finalizedBy(copyTask) }
        }
    }
}

or as Kotlin DSL variant

android.applicationVariants.configureEach {
    val variant = this
    variant.outputs.configureEach {
        if (outputFile != null && outputFile.name.endsWith(".apk")) {
            val assembleTask = variant.assembleProvider
            val mustRunAfterTaskName = "create${variant.name.capitalize()}ApkListingFileRedirect"
            val newTargetName = "${theAppName}-v${variant.versionName}-${variant.name}.apk"
            val copyTask = project.tasks.register("copyAndRenameOutputAPKFile${assembleTask.name.capitalize()}") {
                mustRunAfter(mustRunAfterTaskName)
                inputs.files(assembleTask)
                outputs.file(rootProject.file(newTargetName))
                doLast {
                    copy {
                        from(assembleTask)
                        into(rootDir)
                        rename(outputFile.name, newTargetName)
                    }
                    println("Copied ${outputFile.name} to ${newTargetName}")
                }
            }
            assembleTask { finalizedBy(copyTask) }
        }
    }
}

Ty very much Vampire for ur reply and suggestions. Appreciating it.
Question: do u know a separate forum/place to ask for Android Gradle Plugin Quesions?

I can’t swap to Kotlin because the tool-chain I must use, generate autom. gradle scaffolfds.

In general my code snippet above does work and does what it’s supposed to do.
Do u could tell me where particularly the “problematic parts” can be found there?

Edit:
as u might see here Problèmes connus concernant Android Studio et le plug-in Android Gradle  |  Android Developers
it’s recommended to use all instead of each ie.

Question: do u know a separate forum/place to ask for Android Gradle Plugin Quesions?

No, I’m not an Android developer.
Here might be ok, but I did not see much Android specific activity here.
Other options might be

Do u could tell me where particularly the “problematic parts” can be found there?

Just compare your version and mine.

  • do not use the eager .all but use the lazy .configureEach
  • do not use the tasks.create but tasks.register to leverage task configuration avoidance
  • do not use string concatenation but a GString
  • do not use a Copy task to copy into the root project directory, that would consider all contents of the root project directory an output of the task and easily make things out-of-date when they shouldn’t and waste time fingerprinting unnecessary stuff and maybe even fail fingerprinting, instead use an untyped task and use the copy { ... } function, and declare exactly that inputs and outputs your task has
  • do not use paths as inputs that are outputs of other tasks, instead use those tasks or their outputs as inputs, then you also do not need explicit dependsOn declarations which are a code-smell (except if the left-hand side is a lifecycle task), but get implicit task dependencies automatically where needed
  • do not use the name of a task of which you already have the TaskProvider, but use the provider to use its output, or depend on it, or configure it
  • do not get a task by name eagerly just to configure it or you disturb task configuration avoidance

as u might see here Problèmes connus concernant Android Studio et le plug-in Android Gradle | Android Developers
it’s recommended to use all instead of each ie.

As you might see there, each != configureEach
each is the Groovy way to iterate over a collection (same as forEach for Groovy or Java).
It is correct that each would only iterate over the current elements while all would also hit future elements.
But configureEach also hits future elements and configureEach is the lazy replacement that only does the configuration if the element is actually realized and configured at all, while all causes all elements to immediately get realized and configured, wasting time unnecessarily.

Ty for so detailed explanations and of course the links.

Let surprise me, that my code-snippet above does work at all :grinning:
so much wrong/bad approachs I have.

Doing a refactor wiil be probably be best.

Well most things were just bad practice, or degrading build performance but not incorrect.
Having the project directory as output directory of a copy task could have made problems, it did in the past, but maybe in the happy path it also did now the right thing and just makes problems in certain situations. :slight_smile:

As I said, I also don’t know whether it is the right approach, as I personally don’t do Android dev, I just cleaned up your snippet. :slight_smile:

You can’t know this, but my target directory is the directory upon the Android root directory:
def rootProjDir = System.getProperty("user.dir") + '/../../'
Because the Android is only one artifact of my multiple-target project.

That’s even worse actually.
You must not use user.dir.
For the same reason you must not use File(...) with a relative path.
The working directory of the build execution currently often is the project directory, but this is in no way guaranteed and also in some situations is not the case.
So any path depending on the current working directory makes your build unpredictable.

And any directory as target of a Copy task that has other files in it is highly questionable and most often not a good idea.
So still, you should use a task that is not a Copy task but defines the one input file and output file and internally use the copy { ... } function as I had shown.

And additionally you should not use the working directory, but rootDir or rootProject.file or similar, I cannot tell exactly from the information you gave.

OMG, how could my code above had ever worked :upside_down_face:
I was so helpless at the beginning, that i was glad about every code-snippet i found that smh had worked for me.

Now I post my complete build.gradle file which is in this dir here:
myProject/src-capacitor/android/app/build.gradle
(before I make any refactoring.)

The aim is to copy the finished assembled APK file with more infos in the filename into this upper path myProject

apply plugin: 'com.android.application'

def appVersion = ""
def theAppName = ""
def packageJSONFilename = System.getProperty("user.dir") + '/../package.json'
def rootProjDir = System.getProperty("user.dir") + '/../../'
def androidDestDir = System.getProperty("user.dir") + '/app/src/main/assets/public/'
def copyPatterns = {
    include 'license_*'
}
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))


Integer appCode  = 1
try {
    def versionJSONFile = file(packageJSONFilename)
    def slurper = new groovy.json.JsonSlurper()
    appVersion = slurper.parseText(versionJSONFile.text).version
    theAppName = slurper.parseText(versionJSONFile.text).name
    def (_,major) = (appVersion =~/^(\d+).*/ )[0]
    appCode =  major as Integer
    println  " Building App ${theAppName} with Version: ${appVersion} with Code: ${appCode}"
} catch(Exception e) {
    logger.warn("could not read version of ${packageJSONFilename}")
    logger.warn("  -> ${e}")
}


android {
    signingConfigs {
        release {
          keyAlias keystoreProperties['keyAlias']
          storeFile file(rootProjDir +  keystoreProperties['storeFile'])
          storePassword keystoreProperties['storePassword']
          keyPassword keystoreProperties['keyPassword']
        }
    }
    compileSdkVersion rootProject.ext.compileSdkVersion
    defaultConfig {
        applicationId "my.project.app"
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode appCode
        versionName appVersion
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        aaptOptions {
            ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
            android.applicationVariants.all { variant ->
              variant.outputs.all { output ->
                def outputFile = output.outputFile
                if (outputFile != null && outputFile.name.endsWith('.apk')) {
                    def assembleTaskName = variant.getAssembleProvider().name
                    def mustRunAfterTask = "create${variant.name.capitalize()}ApkListingFileRedirect"
                    def newTargetName = theAppName + "-v" + variant.versionName + "-" + variant.name + ".apk"
                    def copyTask = project.tasks.create("copyAndRenameOutputAPKFile$assembleTaskName", Copy)
                    copyTask.from(outputFile.parent) {
                      include outputFileName
                    }
                    copyTask.into(rootProjDir)
                    copyTask.rename outputFileName , newTargetName
                    copyTask.dependsOn assembleTaskName
                    copyTask.mustRunAfter mustRunAfterTask
                    copyTask.doLast() {
                      println "Copied ${outputFileName} to ${newTargetName}"
                    }
                    tasks[assembleTaskName].finalizedBy = [copyTask]
                }
              }
            }
        }
    }
}

repositories {
    flatDir{
        dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
    implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
    implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
    implementation project(':capacitor-android')
    testImplementation "junit:junit:$junitVersion"
    androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
    androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
    implementation project(':capacitor-cordova-android-plugins')
}

task copyExtraFiles(type: Copy) {
    from file("${rootProjDir}") , copyPatterns
    into file("${androidDestDir}")
}

apply from: 'capacitor.build.gradle'

try {
    def servicesJSON = file('google-services.json')
    if (servicesJSON.text) {
        apply plugin: 'com.google.gms.google-services'
    }
} catch(Exception e) {
    logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}

preBuild.dependsOn(copyExtraFiles)

The most is done by the scaffold. Only the copy tasks and the first try-catch I’ve added by myself.

Don’t use exceptions for flow control.
That is bad practice also in Java or any other code.
Exceptions are for exceptional cases, not for expected situations, hence their name.

def servicesJSON = file('google-services.json')
if (servicesJSON.file && servicesJSON.text) {
    apply plugin: 'com.google.gms.google-services'
} else {
    logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}

:slight_smile:

This part is not from me.
It’s done by the scaffold maker (Capacitor or QuasarCLi).

Well, whatever “scaffold maker”, “Capacitor”, and “QuasarCLi” are, that doesn’t make it any better. :smiley:
But I actually missed the “first” and thought that was yours. :smiley:

You are right. But I have no influence on it.
Creating a new project this tool chain will result in the same output gradle files.

Now i found the time to investigate more and do some refactoring.
But I’m stumbled now over these 2 lines above of ur first proposal, which I really don’t understand in that way used.

Appreciating furhter explanations.
Thx in advance.

Which part do you not understand?
those define the inputs and outputs of the task, for example for up-to-date checks.
The inputs are the outputs of the assembleTask, the output is the file you copy to.

Sorry, but I still do not understand it

If outputs.file() does regard the newTargetNane
why must it still be renamed after copying?!

And does it has to be inputs.files(assembleTask) or only inputs.file(assembleTask) (w/o “s” )?!
I don’t understand the difference here.

I would have done now smt as this:

def rootProjDir = "${rootDir}/../../"
...
                  def assembleTask = variant.getAssembleProvider()
                  def mustRunAfterTaskName = "create${variant.name.capitalize()}ApkListingFileRedirect"
                  def newTargetName = "${theAppName}-v${variant.versionName}-${variant.name}.apk"
                  def copyTask = project.tasks.register("copyAndRenameOutputAPKFile${assembleTask.name.capitalize()}") {
                    mustRunAfter(mustRunAfterTaskName)
                    doLast {
                      copy {
                        from outputFile.parent
                        into rootProjDir
                        include outputFileName
                        rename outputFileName , newTargetName
                      }
                      println("Copied ${outputFileName} to ${newTargetName}")
                    }
                  }
                 assembleTask { finalizedBy(copyTask) }      // this line here is mentioned by the error

But i when start this above , i’m getting always this unreadable and not understandable error message:

org.gradle.internal.metaobject.AbstractDynamicObject$CustomMessageMissingMethodException: Could not find method call() for arguments [build_5l9hzaqycvuc6fy8piedndxy5$_run_closure2$_closure8$_closure11$_closure12$_closure13$_closure15@7f2cfe53] on provider(task 'assembleDebug', interface org.gradle.api.Task) of type org.gradle.api.internal.tasks.DefaultTaskContainer$TaskCreatingProvider_Decorated.

If outputs.file() does regard the newTargetNane
why must it still be renamed after copying?!

Just that you tell the task which file will be its output file, does not mean the task action magically knows how to behave and that the specific file needs to be renamed.

And does it has to be inputs.files(assembleTask) or only inputs.file(assembleTask) (w/o “s ” )?!
I don’t understand the difference here.

Just what you would expect
files = multiple files
file = single file
But the entirety of a tasks output files in general is plural as it could be multiple, even if a specific task just has one file as output, hence you use files, not file which would complain.
Just look at the JavaDoc of both methods and you will see what is supported for either.

I would have done now smt as this:

Well, of course you can ignore half of what I say, you just don’t get a good result.
Your task will for example never be up to date because you do not define its inputs and outputs, and using “from / include” when you can simple copy from the task outputs is just ugly, even though it would work if you would at least have declared the task as input.
WIth not defining the inputs, you will also not have the task run before and thus the file to be copied could be missing or stale.

But i when start this above , i’m getting always this unreadable and not understandable error message:

Well, even if you cannot follow it, I have to repeat my recommendation. Use Kotlin DSL. Besides type-safe build scripts and amazingly better IDE support, you will especially get helpful error messages if you mess up syntax, while you get such unhelpful messages when using Groovy DSL. It means something in the closure after assembleTask is incorrect. In your case it is just one line, but this could be also a hundred lines and you don’t get any helpful information where the actual error is, while you would with Kotlin DSL.

Actually in this case the error is, that the Groovy DSL is not as nice as the Kotlin DSL, you need to use assembleTask.configure { finalizedBy(copyTask) } instead.

Ty.
As i worte, I cannot change tool-chain to the usage of kotlin files.

That’s why I wrote “even if you cannot follow it”. :slight_smile:
Besides that there are always ways and be them to convince the toolchain provider to use Kotlin DSL instead. :smiley: