Closed mgroth0 closed 2 months ago
Hi, it is not worth using task types for dependencies, because for example, in Android, several tasks with the same type can be created (in the future this may be added to regular JVM projects).
You may add a dependency to a Kover task by its name, for example
tasks.myTask { dependsOn(tasks.named("koverHtmlReport")) }
Names koverXmlReport
, koverHtmlReport
, koverVerify
are stable for Kotlin/JVM or Kotlin/MPP, сonstants for the names of these tasks will be introduced in 0.7.0
.
Hey @shanshin. Thanks for the info. I disagree with your opinion on this.
Making task types public API is the norm. I've never encountered a gradle plugin before this that doesn't have public task classes, regardless how many tasks are created per project.
Depending on tasks using withType
is more typesafe and preferred.
Configuring tasks using tasks.withType<X>.configureEach
is also idiomatic.
It seems to me making task types public API has great benefit, no cost, and aligns better with idiomatic gradle usage.
The described problem is solved by searching for a task by name (tasks.named("someName")
), while it is also safe because if there is no task with that name, an error will be thrown.
The use of tasks.withType<X>.configureEach
in the case of Kover tasks in most cases is not assumed, because all the configuration of the plug-in takes place in a single place - extensions of the project.
For rare cases, a search by name can be used, which is generally preferable, because in Android projects most often work will be carried out with a specific build variant and the corresponding task.
I've never encountered a gradle plugin before this that doesn't have public task classes, regardless how many tasks are created per project.
In fact, there are quite a lot of such tasks, for example, the most frequently used such task is check
from the base
plugin.
In the future, some public classes may be added, however, it is necessary to understand what problem they will solve.
At the moment, the only known problem is defining dependencies between tasks, but it is easily solved with the names. For kts build scripts, we will add type-safe accessors for default reports so that users can write tasks.koverHtmlReport
.
tasks.named("someName")
is less safe because it only throws an error at runtime, not compilation time. Compilation-time type safety is cleaner and more robust. Type safety is also reinenforced by IDE syntax highlighting, but finding by names is not.
If I want to reference "koverHtmlReport" in multiple places, I will likely want to define const val KOVER_HTML_REPORT_TASK_NAME = "koverHtmlReport"
somewhere. But that's just extra clutter in my code where tasks.withType<KoverHTMLReport>
would be more elegant and simple.
Checking if a task is a kover task based soley on the name will not work in when
statements:
when (task) {
is KotlinCompile, is Jar -> {
...
}
is JavaExec -> {
...
}
.name == KOVER_HTML_REPORT_TASK_NAME -> { // compilation error
...
}
}
(at least for now, until one day KT-21661 is hopefully implemented )
Finding objects by type rather than name is also useful because if the hierarchical structure. If all kover tasks can implement an empty interface interface KoverTask: Task
, then its easy to gather all kover tasks with tasks.withType<KoverTask>
. KoverHtmlTask
and KoverXmlTask
can both implement a KoverReport
interface (that is not implemented by KoverVerifyTask
) if a user only wants to gather the tasks that's main purpose is to generate a report.
The kotlin-gradle-dsl is not available in all places. For example, it is not included by default in the gradle API when writing gradle plugins (and it can be challenging to include the kotlin-gradle-dsl as a dependency for gradle plugins.) So type-safe accessors won't solve this issue as broadly as public classes would.
Allowing users to use tasks.withType
does not prevent the from using tasks.named
. tasks.named
, kotlin-gradle-dsl accessors, or other techniques can still be used to find tasks for a particular android build varient or similar.
check
is a lifecycle task. AFAIK it does not do anything, it only depends on other tasks. I'd be curious if there are any tasks that actually do actions that don't have some public type it can be referenced, or at least narrowed down by.
I don't think new classes or code needs to be written (though creating a simple hierarchy with interfaces would be excellent). What issues would be introduced by simply removing the internal
keyword for the existing task classes? The internal
keyword could be added to the constructor and/or to any properties to protect the integrity of the classes if needed. As far as I can see, this change would have no effect on the rest of the plugin
Given how small of a change this is that there are identifiable benefits, I'm curious why there is such a need for justification. What costs are there to simply removing the internal
keyword?
Given how small of a change this is that there are identifiable benefits, I'm curious why there is such a need for justification. What costs are there to simply removing the internal keyword?
It immediately becomes a part of public API which imposes quite a lot of restrictions:
val someTrickFlagAsWorkaround
, but with public API we can'tNote that all these problems are solvable and are not arguments against your proposal, but rather an answer to "why it's not public by default" question.
To the original report -- thanks for the effort and for going the extra mile in explaining to us why this is a problem for Kover's users! We'll discuss it internally and see what we can do to both solve your issue and keep our public API minimal
tasks.named("someName") is less safe because it only throws an error at runtime, not compilation time.
This problem is solved by using constants with the task names: e.g. kotlinx.kover.api.KoverNames.HTML_REPORT_TASK_NAME
for 0.6.1
What issues would be introduced by simply removing the internal keyword for the existing task classes?
Unfortunately in this case AbstractKoverReportTask
becomes public, however these are implementation details. This class does not fit the hierarchy of reports described above (verify is also inherited from it), however, by making it public, we will need to maintain exactly such a hierarchy in the future for compatibility, which makes refactoring difficult.
Finding objects by type rather than name is also useful because if the hierarchical structure.
At this point, there may be discrepancies in the understanding of the correct hierarchy. After the implementation of #229, new tasks for printing "reports to logs" will appear, and verification will start working in a similar way after #339.
Allowing users to use tasks.withType does not prevent the from using tasks.named. tasks.named, kotlin-gradle-dsl accessors, or other techniques can still be used to find tasks for a particular android build varient or similar.
in this case, Android users can find an example of a solution with withType
on the Internet and use configureEach
to configure several tasks at once without realizing it.
If the concepts of default reports (always existing tasks with names koverHtmlReport
, koverXmlReport
etc) remain in the stable version , then it will be possible to add the interfaces like KoverDefaultHtmlReport
. However, this requires additional design in the future.
I appreciate the responses. I've never authored a public library before so it is helpful for me to learn about the types of design challenges you confront when considering what to make public API. Thanks both of you for for engaging with me and educating me on this!
One thought I had was Opt-in requirements. You could create:
@RequiresOptIn
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class IncubatingKoverAPI
And annotate the class with this. I think this could help in a few ways:
Opt-in requirements seems like a nice catch all solution that could address a number of the concerns you both raised above.
in this case, Android users can find an example of a solution with withType on the Internet and use configureEach to configure several tasks at once without realizing it.
I think this might be ok if it is consistent with other tasks and the norm in the Android ecosystem. I suspect a significant portion at least (if not the majority) of Android Gradle Plugin tasks are both public API (can work in withType
) and also constructed with multiple instances per project (one per build varient). If this is the norm in the AGP, or even if its common enough that Android developers generally should know to look out for this, then perhaps its not your responsibility to safeguard android devs from making that mistake. I suspect even outside of the AGP, this may be common. For example, the class JavaCompile
must have a seperate instance for each source set (like "main" and "test") within a project. I think syntax tasks.withType
is explicit since "tasks" is plural that you may very well be configuring multiple tasks.
In the end I'll respect the design choices you make. Thank you for clarifying your reasoning.
I agree that making the tasks internal
is highly unusual, and in the case of Groovy build scripts has no effect, since Groovy can ignore visibility modifiers.
// build.gradle
import kotlinx.kover.gradle.plugin.tasks.reports.KoverHtmlTask;
plugins {
id "org.jetbrains.kotlin.jvm" version "1.8.21"
id "org.jetbrains.kotlinx.kover" version "0.7.1"
}
afterEvaluate { // required because of https://github.com/Kotlin/kotlinx-kover/issues/394
tasks.named("koverHtmlReport", KoverHtmlTask).map {
it.reportDir
}
}
Another usecase for exposing the task types is to be able to access the input/output properties in a typesafe way. For example, I might want to use the files that a Kover task generates as the input for another task https://docs.gradle.org/8.1.1/userguide/incremental_build.html#sec:task_input_output_side_effects. Because the classes are private then I can't do this.
For example, I might want to use the files that a Kover task generates as the input for another task https://docs.gradle.org/8.1.1/userguide/incremental_build.html#sec:task_input_output_side_effects. Because the classes are private then I can't do this.
You may explicitly specify the report output directory and add a dependsOn("someKoverTask")
- this will be enough for Gradle.
Until the DSL stabilizes, this will be more efficient than getting migration errors when changing the implementation of Kover tasks classes.
You're right, I shouldn't have said "I can't do this".
However, because the classes are private it makes it more difficult to discover this behaviour without digging through the source code or being very savvy with Gradle. It also makes it more difficult to perform actions on a tasks output, e.g. filtering the output
val someSpecificKoverFiles = tasks.someKoverTask.map {
it.reportDir.asFileTree.matching { include(...) }.files
}
Also, as you have indicated you might refactor the task - so are you sure it's safe to use dependsOn("someKoverTask")
? The task name or output could change without notice.
and in the case of Groovy build scripts has no effect, since Groovy can ignore visibility modifiers.
Java reflection is also available, but the question here is not to exclude the technical possibility of accessing the class, but to ensure that users do not randomly start using implementation details for which we do not guarantee backward compatibility.
It also makes it more difficult to perform actions on a tasks output, e.g. filtering the output
This is a good use case, the outputs of the task can be listed in the public interface, but the inputs should not be specified when the configuration is carried out through the project extension.
Also, as you have indicated you might refactor the task - so are you sure it's safe to use dependsOn("someKoverTask")? The task name or output could change without notice.
This approach is still under development, but it seems to me that constants should be declared for Kover default tasks names, and for Android tasks it is necessary to create a public function like koverHtmlreportTaskName(buildVariantName)
This is a good use case, the outputs of the task can be listed in the public interface, but the inputs should not be specified when the configuration is carried out through the project extension.
I strongly disagree that Kover has to be so defensive with regards to protecting task properties. Manually modifying the inputs is sometimes necessary. To give one example from a different plugin: to help with integration tests.
These properties are already exposed to the public in Groovy scripts, so keeping the tasks as private just makes gradle.kts
scripts inferior.
I disagree that Java reflection to access private code is comparable to Groovy - the former is complicated and the latter is easily accessible, and is even suggested via auto-completion.
The approach at the moment is encouraging using random implementation details. Because of using stringly-typed task names, inferring task outputs, and afterEvaluate {}
right now I'm writing non-type-safe scripts that will degrade over time, and are missing out on unbelievably helpful functionality like @Deprecate
, ReplaceWith
, and @ReplacedBy
.
This approach is still under development, but it seems to me that constants should be declared for Kover default tasks names, and for Android tasks it is necessary to create a public function like
koverHtmlreportTaskName(buildVariantName)
I understand that dealing with Android is complicated, but thought of having to deal with a plugin that is breaking the norms that all other plugins follow is exhausting. Using Gradle is already difficult as it is. I shouldn't have to leave my IDE in order to figure out how Kover wants me to use Gradle.
In addition, when writing custom plugins and reacting to other plugins being enabled. Having to guess the task name is not an effective solution. Being able to say "if this task is present in the task graph, finalize it with this task", so that a custom plugin can post process the report generated. With the current behavior I have to duplicate the logic this plugin uses to generate task names, so I can iterate through them to create the necessary dependencies and finalizers.
Implemented in 0.8.0-Beta
.
This is not a production ready release, the plugin DSL is experimental and can be changed after receiving feedback.
@shanshin Thank you for all of your effort on this and the plugin in general. It is looking great!
However, I do not think that 0.8.0-Beta
resolves this issue.
/**
* Common interface for all Kover report tasks.
*/
interface KoverReport
The issue is that this interface and all other task interfaces need to implement org.gradle.api.Task
. If it doesn't we cannot use the common pattern of:
tasks.withType<KoverReport>.configureEach {
}
The type argument for withType
has Task
as an upper bound.
Should we reopen this while we sort this out?
Thanks!
Make sense to extend org.gradle.api.Task
in KoverReport
.
On a related note, how about exposing TaskProvider
instances inside the configuration DSL?
In the Kotlin Multiplatform Gradle Plugin extension, there are public TaskProvider
values. For an example see org.jetbrains.kotlin.gradle.plugin.mpp.NativeBinary.linkTask
:
kotlin {
macosArm64 {
binaries.configureEach {
// linkTaskProvider is a `TaskProvider<out KotlinNativeLink>` provided by the kotlin plugin
myCustomBuildTask.dependsOn(linkTaskProvider)
linkTaskProvider.configure {
dependsOn(myCustomCheckTask)
}
}
}
}
Having public values exposing TaskProvider
instances for kover tasks might become increasingly useful as Kover begins to support more kotlin platforms.
If you want I can create a separate issue for this, but it falls within the same umbrella of having public APIs for getting references to Kover tasks.
Could you clarify use case where TaskProvider
needed, and the provider of which Kover tasks?
Sure.
Use case: I might make my own custom aggregation task per platform for different forms of checks. For example:
abstract class MyCustomAndroidChecks: DefaultTask() {
}
val myCustomAndroidChecks by tasks.registering<MyCustomAndroidChecks>()
The purpose of MyCustomAndroidChecks
might be to aggregate checks from different plugins. For example, I might aggregate kover with detekt or ktlint.
So, I might want to make myCustomAndroidChecks
depend on specific tasks, such as koverVerifyDebug
or something. I cant just make myCustomAndroidChecks
depend on all instances of KoverVerifyReport
because I do not want it to depend on the jvm variant.
It could also be the other way around. Like if I wanted the android variant koverVerifyDebug
to depend on some specific android task that generates android code, for example.
So exposing TaskProvider
s in the way that kotlin multiplatform plugin does is really nice because it is a typesafe way to do this type of configuration without having to rely on string names for tasks, which can have all sorts of issues.
Which tasks:
Currently I use the koverVerify
, koverHtmlReport
, and koverPrintCoverage
tasks, for both android and jvm-desktop varients.
For Kotlin JVM project you already can use tasks.koverVerify
, tasks.koverHtmlReport
etc.
For Android projects we can add extensions like fun TaskContainer.koverVerify(variantName: String): TaskProvider<>
to use it this way tasks.koverVerify("debug")
(applicable for kts
build scripts).
*However, I am not sure that this will be easy to implement, because the list of variants is determined at the evaluation stage and these tasks are created at the end of the stage, respectively, we can't use the function named("taskName")
Thanks, I did not know about the existence of things like tasks.koverVerify
. However, for me this is only working in a buildscript. I compile the kover gradle plugin as a normal dependency for my gradle plugin and for some reason the kotlin dsl accessor is not available. I do not fully understand how these accessors work, and why I cannot resolve them when using kover as a normal dependency. My plugin also has a dependency on the kotlin dsl so I can do things like tasks.registering
but I still cannot resolve tasks.koverVerify
for unknown reasons.
For the problem about android variants, what about exposing a DomainObjectSet
or a similar live gradle collection for each variant? Each variant would have a reference to a TaskProvider for each task on the variant, and can also have a simple val variantName
. This would work in the same way as org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget#binaries
(and many other similar features of the kotlin gradle plugin)?
This would work in the same way as org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget#binaries (and many other similar features of the kotlin gradle plugin)?
Comparing with the KMP plugin, you miss a small detail that in KMP there is both the creation of a target and access to its provider in one place.
E.g. for example
kotlin {
macosArm64 {
binaries.configureEach {
someUsage(linkTaskProvider)
}
}
}
you take the task in the extension in which you actually create it.
When it comes to Kover + AGP, the build variants are specified in AGP, in Kover we cannot know in advance which variants exist and which do not (in some cases they may be created at the afterEvaluate
stage).
Accordingly, even if, for example, we provide such a DSL
kover {
reports {
variant("debug") {
htmlReportProvider: TaskProvider<KoverHtmlReport>
}
}
}
then it is necessary to write a custom implementation of TaskProvider
, because the task will be created much later than the access to the provider will take place.
As a compromise option, I can suggest adding the variantName
property to the KoverReport
type, so that you can write such configurations
val koverVerifyTasks = tasks.matching {
it is KoverVerifyReport && it.variantName in setOf("debug", "release")
}
or
val koverVerifyTasks = tasks.withType(KoverVerifyReport::class.java).matching {
it.variantName in setOf("debug", "release")
}
myTask.configure {
dependsOn(koverVerifyTasks)
}
Do you know how I might be able to use tasks.koverVerify
inside of my gradle plugin? I was hoping you might be able to point me in the right direction, just because I am curious about how exactly the kotlin dsl code is generated. But no worries if you aren't sure, as this isn't going to be a full solution for this issue anyway.
I am not quite understanding the issue you explain above. Let me propose this:
kover {
variants.configureEach {
verifyTaskProvider.configure {
}
}
}
Here, varients is live collection populated lazily. It shouldn't matter that it is created using information from the AGP configuration, or even if it is populated inside of a afterEvaluate
call. Gradle collections are designed to be populated and configured lazily.
Is this not possible? Perhaps I am still missing something.
Ideally variantName
would be a property of the Variant
configuration object instead of the task. It would be located next to the task provider, not inside of the task.
This seems better than relying on avariantName
inside the Task because then do not ever have to realize tasks.tasks.matching
is best avoided.
But I do see your point about Android variants not being hardcoded. This was my mistake. I see that it would be impossible to hardcode accessors for specific android variants.
Do you know how I might be able to use tasks.koverVerify inside of my gradle plugin?
Unfortunately, your plugin will not be able to use this property, because this DSL is available only inside kts
files.
Is this not possible? Perhaps I am still missing something.
Technically, this is possible, but there are several difficult points.
Firstly, there is currently no publicly available Variant
domain object in Kover.
If we implement a collection of such objects, the case you have given is not very well suited for such an usage:
collection.configureEach {
is intended primarily for configuring the element of the collection itself, its settings, but not getting the value of any properties from this element.
In addition, it is not always obvious that the action specified in configureEach
is performed "sometime later" (or never at all) - this can lead to implicit errors if code is written there that must be executed at the evaluate
stage.
And it doesn't seem to me that such code
kover {
reportVariants.configureEach {
if (variantName in setOf("release", "debug")) {
myTask.configure {
dependsOn(htmlReportProvider)
}
}
}
}
would be clearer than
val koverHtmlTasks = tasks.withType(KoverHtmlReport::class.java).matching {
it.variantName in setOf("debug", "release")
}
myTask.configure {
dependsOn(koverHtmlTasks)
}
You make a great point that configureEach
would not be best if there were such a domain collection of variants
. It might be better to use all
, or an "implicit" all
, which resolves these domain objects eagerly:
kover {
reportVariants.all {
if (variantName in setOf("release", "debug")) {
myTask.configure {
dependsOn(htmlReportProvider)
}
}
}
}
kover {
reportVariants {
if (variantName in setOf("release", "debug")) {
myTask.configure {
dependsOn(htmlReportProvider)
}
}
}
}
That is because like you say, it is unclear with configureEach
if the configuration would be triggered for arbitrary domain objects.
This would match the kotlin multiplatform plugin DSL. All of these are technically possible:
binaries.configureEach { }
binaries.all { }
binaries { }
Though normally, we would only use binaries {}
or binaries.all {}
. I have not seen binaries.configureEach { }
used.
As for matching
, I think this is actually a performance issue not code cleanliness issue. I agree that this code looks clean:
val koverHtmlTasks = tasks.withType(KoverHtmlReport::class.java).matching {
it.variantName in setOf("debug", "release")
}
myTask.configure {
dependsOn(koverHtmlTasks)
}
Though matching
has some issues related to realizing instead of lazily constructing and configuring them. The gradle docs currently say matching
is "OK, with issues", but this might not be entirely accurate as there are even more undocumented issues with matching
. There is an open issue about it: https://github.com/gradle/gradle/issues/26047
I believe that if there is any possible alternative to matching
, that using the alternative would be correct in in order to have better task configuration avoidance?
Yes, matching
has some problems, but the effect on performance will be negligible because in the example I mentioned, withType
is used as in the Gradle recommendation.
Since in your case you wanted to take almost all variants except JVM, then a minimum of unnecessary tasks will be created (for example, only for jvm
and total)
Agreed. Using withType
greatly mitigates the performance issue. I'm just being a perfectionist 😮💨
Well, is DSL like (in 0.8.0-Beta version)
kover {
reports {
for (variantName in setOf("release", "debug")) {
variant(variantName) {
myTask.dependsOn(htmlReportProvider)
}
}
}
}
suitable for you?
I will need to get the variant for each Kotlin Multiplatform target. I am hopeful this library will eventually support all targets (I know it might still be a while). But if it ever does, I imagine there will be many variants. Referencing them by string will not seem very stable.
Some ideas:
Variant
domain collection, as we have discussed abovevariant(target)
which takes a org.jetbrains.kotlin.gradle.plugin.KotlinTarget
as a parameterIs my understanding correct that there would be a variant per target, not per source set? As in, there would never be a "koverCommon" variant, correct?
It's about current needs. Supporting all multiplatform targets is a huge task that will take a very long time to develop. It is not known what DSL will be then, and whether it will be a separate Gradle plugin. Therefore, it is worth not to go so far.
Absolutely understandable. I think if kover task interfaces now extend from Task
, this issue can be closed. We can tackle other things in new issues at a later date. Thank you for taking all of my thoughts into consideration.
Thanks!
I will still add variantName
property to the KoverReport
interface.
This may not be an ideal option, but right now there is no other way to filter Kover tasks other than analyzing the task name and this is being implemented quickly.
Fixed in 0.8.0-Beta2
Relates #587
I am new to this plugin and I wanted to make certain tasks depend on Kover tasks. However,
KoverHtmlTask
and such areinternal
. This would be fine as long a superclass or interface were made public. But evenAbstractKoverReportTask
is internal. There is no straightforward way to dotask.dependsOn(tasks.withType<KoverHtmlTask>())
.There are a couple of simple solutions. One is that you can make these tasks public and make the constructor internal. Another is that you can create a public interface that they extend from.