I had this piece of build configuration: ```targets { configureEach { tasks.named(targetName +...
e
I had this piece of build configuration:
Copy code
targets {
  configureEach {
    tasks.named(targetName + "SourcesJar").configure {
      dependsOn(generateCharsets)
    }
  }
}
With 2.2.20, I was getting a deprecation message that says: > [DEPRECATION] 'targets(TargetsFromPresetExtension.() -> Unit): Unit' is deprecated. Usages of this DSL are deprecated, please migrate to top-level 'kotlin {}' extension. After a bit of searching on my own I figured out that I just had to use:
Copy code
targets.configureEach {
  tasks.named(targetName + "SourcesJar").configure {
    dependsOn(generateCharsets)
  }
}
The deprecation definitely did not help. It seems too generic to be honest.
v
Why? It just tells you that
targets { ... }
is deprecated. That's not really generic but rather specific imho. Besides that you should most probably not do what you do there anyway.
Any explicit
dependsOn
that does not have a lifecycle task on the left-hand side is a code smell and almost always a sing that you do something wrong, most often that you do not properly wire task outputs to task inputs which automatically brings the necessary task dependencies. If you for example have a task called
generateCharsets
that generates sourcecode, then you should use that task itself (or a provider thereof) as
srcDir
, given it properly declares its outputs as it should anyway. Then all properly behaving consumers of sources, including source jars automatically have the necessary task dependency.
e
That's not really generic but rather specific imho.
It's telling me to use
kotlin {}
, which I'm already using. Would be better to show an example of
targets.configureEach
instead imo.
Re. the task wiring, you're right, thanks! That's something I'll do next.
o
Any explicit
dependsOn
that does not have a lifecycle task on the left-hand side is a code smell
If I'm adding a generated file to a Kotlin source set, I must also add an explicit task dependency. Am I correctly assuming that this is an exception to the above "code smell" rule?
c
@Oliver.O In theory the simplest way would be to add the task itself to srcDir, which would imply both already. Or, you can use
.buildBy
to tell a directory which task creates it, but I think the first option is preferred.
thank you color 1
o
Interesting. Yes, maybe I should do that.
v
It's telling me to use
kotlin {}
, which I'm already using. Would be better to show an example of
targets.configureEach
instead imo.
Ah I see what you mean. I don't think it can show
targets.configureEach
as example as the complaint is about using
targets { ... }
, no matter what is inside, it cannot know that. But I would agree that saying to use
kotlin { ... }
while being inside that block just one level too deep is a bit weak as suggestion @tapchicoma 🙂
Am I correctly assuming that this is an exception to the above "code smell" rule?
No, as @CLOVIS said and I said before, you should make sure the task properly declares its outputs and then use the task itself as
srcDir
or one of its output properties that is set up properly if the task has additional outputs. That construct is not an exception, but the most typical case of what is done wrong, configuring explicit paths instead of properly wiring task outputs.
thank you color 1
Also for a "normal" task where you configure the output location on the task instance, this could also easily break if a consumer reconfigures the task output location which is then not registered as
srcDir
while the task dependency is still there due to configuring it manually.
o
I normally do task input/output wiring. I just wasn’t aware that source sets behave like inputs. They seemed like a disparate concept to me. I‘m AFK at the moment, but Gradle‘s type system doesn’t make a source set a derivative of input, right?
v
Dunno what you mean by "a derivative of input". I don't have the internals in mind right now, but I guess by calling
srcDir()
you - at least conceptually - add the argument to
FileCollection
or
FileTree
which preserve the task dependency if present and those are then probably wired to something like the inputs of a sources
Jar
task and the inputs of a compilation task. So technically you do not wire the task outputs to inputs directly, as you are not configuring a task. But you put the outputs to a collection that then later is used as inputs in tasks.
e
What if the task outputs to multiple dirs (e.g.
build/generated/jvmMain
,
build/generated/nonJvmMain
)? In that case I can't directly use it as
srcDir
, because I need to split by KMP source set.
o
Dunno what you mean by "a derivative of input".
class SourceSet : Input
Then it would be clear.
v
What if the task outputs to multiple dirs
As mentioned above, the task should optimally provide multiple output properties for those locations, and you would then use that output property as
srcDir
. Output properties inherit the task dependency.
Then it would be clear.
Not really.
Input
is an annotation, that would be a quite strange inheritance. 🙂 And besides that, as I said, the source set is not an input. It is a collection of locations that is then later used as input for a task. Imagine it like
Copy code
val outputProducingTask1
val outputProducingTask2
val outputs = listOf(
    outputProducingTask1,
    outputProducingTask2
)
val inputConsumingTask {
    theInput.from(outputs)
}
You could theoretically also use the source dirs in a way no task dependency is triggered like iterating over them at configuration time or similar. But usual consumers properly use the source dirs by wiring them to a task input and there the task dependencies are then adhered to.
e
the task should optimally provide multiple output properties for those locations
Ahhh got it, thanks. Yes, it does, so I was on the correct path.
👌 1
o
> Then it would be clear.
>
Not really.
Input
is an annotation, that would be a quite strange inheritance. 🙂
I wanted to refer to the concept, not technicalities. Inheritance means something to brains.
And besides that, as I said, the source set is not an input.
I get that. The thing is: Having something that’s almost the same but not the same is where it becomes difficult to learn.
v
Again, the source directory set is just a collection of things that happens to later be used as task inputs. The source directories sets cannot know how they are used later on, they are just a "mutableListOf(...)" (overly simplified of course). So there is nothing the source directory set could tell you to hint at that. It is simply the case that all non-misbehaving consumers do use the contents in a way that preserves task dependencies.
o
The solution proposed by @CLOVIS and you is clear, and it works like I always wanted to. As I had formerly been actively researching such a possibility (coupling task outputs to a source set), but could not find one, the question remains: Where did you learn of the possibility that a task (along with its outputs) can be added to a source set?
e
It's probably somewhere in the docs. My complaints with Gradle APIs is many of them accept
Any
and I'm like "well, what's the real allowed types?".
plus1 2
b
the source directory set is just a collection of things that happens to later be used as task inputs.
Implicitly, then, the source directory set must be a collection of things that can later be used as task inputs. Not everything can, and the static types of the relevant methods don’t help you know what can and what cannot. For example, we have SourceDirectorySet.srcDir(Object). The formal argument type, Object, promises more than it delivers: not every Object will work here. Does a Task work here? Does an @OutputDirectory-annotated property work here? How about a literal String? Experienced Gradlers know that all of these work, but the static type of this method’s formal parameter doesn’t make that obvious. Consider what a novice would need to do to learn what can and cannot be used as a source directory. Perhaps they’ve found the SourceDirectorySet DSL documentation, which is quite vague about what srcDir(Object) actually accepts. From there, they head to the SourceDirectorySet API documentation, and scroll to the srcDir(Object) method documentation. There they find prose explaining that “The source directory […] is evaluated as per Project.files(Object...)”, so they go read the Project.files(Object...) method documentation. There they finally find a clear list of what they can actually use as a source directory. The “evaluated as per Project.files(Object...)” pattern applies to many Gradle APIs. Gradle experts eventually internalize most or all of the list of what works in those contexts, or at least recognize when to refer back to the corresponding documentation. But for a beginner, a loosely typed API like SourceDirectorySet.srcDir(Object) just doesn’t give much guidance. I wish that the static type system helped more here, but it doesn’t. I think that’s the point that @Oliver.O was trying to make. Some of that is due to limitations of Java’s type system, such as the lack of Haskell-style type classes. Probably some is due to backward compatibility. If one were starting from scratch, I think one could do better, even within the limitations of what Java static types can express.
thank you color 1
➕ 1
o
Thanks! Hinting at
Project.files(Object...)
is helpful. While I wouldn't call myself a Gradle beginner, I actually am a casual creator of Gradle plugins with limited time I can spend on research. So when I'm reading the docs of SourceDirectorySet.srcDir(Object srcPath) saying
srcPath
- The source directory.
the buck stops right there. A task can never be a directory, so I have to reason to be interested in internals that follow:
This is evaluated as per
Project.files(Object...)
And yes, a properly typed API would help tremendously. But we already knew that.
plus1 1
v
the question remains: Where did you learn of the possibility that a task (along with its outputs) can be added to a source set?
I don't remember, I'm using Gradle since pre-1.0. 😄 But the JavaDoc tells that the argument is evaluated as per
Project.files
like many similar APIs that take
Object
do. But I agree that it could be explained a bit more clear, especially with the generated code use-case in the docs. Seems to be a good candidate for a documentation improvement request. 😉
But for a beginner, a loosely typed API like SourceDirectorySet.srcDir(Object) just doesn’t give much guidance.
Yes, that's unfortunately true. Most probably this is due to in the beginning Groovy being the only DSL and its duck-typing. Maybe not, I have no idea. In those "good old days" it was also normal to accept
Object
for a task input and then later internally use
Project#file
or
Project#files
to resolve it like the built-in APIs do. Nowadays you usually have better typed stuff like
RegularFileProperty
,
ConfigurableFileCollection
and so on. So things are improving, but you always have to have an eye on backwards compatibility. The fast pace with which Gradle and its APIs are still evolving is already ammunition for Gradle-dislikers to argue in favor or Maven and friends. 🙂
plus1 1
o
Arguing in favor of Maven seems a bit strange to me. 😉
v
Always
c
> Where did you learn of the possibility that a task (along with its outputs) can be added to a source set? It seems to be the case that you can put a task more or less anywhere files are expected and Gradle will figure out it means the task's output, but it's kinda a guess tbh
thank you color 1
b
it's kinda a guess tbh
To upgrade that guess to certainty, follow the steps in this message, in the paragraph that starts “Consider…”. You’ll eventually end up at the Project.files(Object…) method documentation, but the journey there may also be instructive.
c
True, that example is documented. In theory though, the same concept should apply to
Copy code
val bar by tasks.registering

tasks.register<Foo>("foo") {
    f.set(bar)
}

abstract class Foo : DefaultTask() {

    @get:InputFile
    abstract val f: RegularFileProperty

}
but that's forbidden. I'm not sure how to make that work.
v
A task per-se has potentially multiple output files, so even if that API would accept
Object
, it could not work, because which file should be wired. But here also you have a more strictly typed API that accepts
File
,
RegularFile
, or
Provider<out RegularFile>
. So as you want to wire with task dependency, you want the
Provider<out RegularFile>
variant. If your task for example has a
RegularFileProperty
as output, you could do
Copy code
abstract class Bar : DefaultTask() {
    @get:InputFile
    abstract val bar: RegularFileProperty
}
abstract class Foo : DefaultTask() {
    @get:OutputFile
    abstract val foo: RegularFileProperty
}
val foo by tasks.registering(Foo::class)
val bar by tasks.registering(Bar::class) {
    bar.set(foo.flatMap { it.foo })
}
If it is a plain old task that does have a
File
as output file, you could still do some mapping like
Copy code
abstract class Bar : DefaultTask() {
    @get:InputFile
    abstract val bar: RegularFileProperty
}
abstract class Foo : DefaultTask() {
    @get:OutputFile
    val foo: File = File("")
}
val foo by tasks.registering(Foo::class)
val bar by tasks.registering(Bar::class) {
    bar.set(layout.file(foo.map { it.foo }))
}
...
🧠 2
e
One last doubt regarding outputs and
srcDir
. Given that registering the task returns a
TaskProvider
, I'm then forced to
get()
it:
Copy code
commonMain {
  kotlin {
    srcDir(myTask.get().commonDir)
  }
}
This doesn't feel correct, although it might actually be.
v
No it is not 🙂
You break task-configuration avoidance that way
srcDir(myTask.flatMap { it.commonDir })
Almost always when you
get()
a
Provider
at configuration time you are doing something bad, most often either breaking laziness needlessly or introducing race conditions similar to then ones you earn from using
afterEvaluate
.
thank you color 1
🧠 1
o
Even in that case?
Copy code
tasks.withType(Test::class.java) {JVM.
    for ((name, value) in providers.environmentVariablesPrefixedBy("TEST_").get()) {
        environment(name, value)
    }
}
v
That does not even compile, there is a stray
JVM.
😄
🥴 1
But ignoring that, this specific case is a bit hard to answer, at least in short.
Environment variables cannot change in the running JVM (let alone some really evil hacks that sometimes work, sometimes not). So there is indeed no race condition in this case.
The only thing that stays with that snippet is, that any change in the
TEST_...
env variables will invalidate the configuration cache
To mitigate that in an easy way,
environment(...)
would need to support `Provider`s which it does not do yet, but hopefully in Gradle 10 does if there finally the great `Property`zatin lands that was announced for Gradle 9.
o
OK, got it! Many thanks!
v
If it were only about the value, one could hack around it by using an object as value that only queries the provider in its
toString
method (the old lazy mechanism some Gradle places support)
👻 1
But as the
name
is also not known, I don't think there is a way to improve that specific snippet for the time being
With
jvmArgs
it would for example be different, because there you could use a
jvmArgumentProviders
instead. 🙂
So yes, you indeed picked one of the few cases where the "almost always" applies currently. 😄
e
srcDir(myTask.flatMap { it.commonDir })
I must say that flat-mapping a task provider was not even remotely in my brain.
b
For the environment variable example, my first inclination would be: 1. at task configuration time, call
providers.environmentVariablesPrefixedBy("TEST_")
and store the returned
Provider<Map<String, String>>
in an
@Input
-annotated field; then 2. in a
doFirst
block, use
get()
to fetch the actual map and copy its contents into the test task's environment. My intent is to postpone calling
get()
on that provider to as late as possible, while still tracking the task's inputs accurately for use with the configuration cache. But given the immutability of JVM environment variables, maybe that's overkill.
v
I must say that flat-mapping a task provider was not even remotely in my brain.
You can also do
srcDir(myTask.map { it.commonDir.get() })
in this case. Here it should have the same result with the same task dependency effect. But
flatMap
indeed is exactly for such use-cases.
gratitude thank you 1
For the environment variable example, my first inclination would be:
The problem is, that means you are changing the task configuration at execution time, and you should never do that. 🙂
I even thought with CC that would be forbidden, but indeed what you described does work
So as long as you make sure the inputs are still properly declared before execution time it might work
b
I guess it comes down to whether calling
Test.environment(Map<String, ?>)
constitutes a change to the task configuration. Now that you point it out, yeah, it seems like it should. But apparently the configuration cache doesn't know that, doesn't mind, or has a different opinion.
o
In the above case, the
TEST_
parameters are used to provide a test filter value which would normally be passed as an argument to
--tests
.
v
Who evaluates them at runtime?
o
The test framework in cases where it doesn't have access to command line arguments.
v
Which?
o
More precisely,
--tests
arguments that would be passed to JUnit Platform.
v
Again, who evaluates
TEST_...
environment variables? JUnit Platform does not.
o
v
Ah, so your own code and it is not only about JVM but KMP. Then using system properties instead is of course not an option. Besides that env variables also do not work for the JS part, do they?
o
In the above case, it's about the JVM part of a KMP framework, invoked via JUnit Platform to be a good citizen in the ecosystem. The same is used on Native, as there is no
main
method in tests where one could pick up command line arguments. On JS, it doesn't have to be used as the filtering is delegated to lower-level JS framworks (KGP uses Mocha). JS/browser obviously doesn't use env vars, JS/Node could.
e
kotlin-test generates code based on the Gradle config to avoid having to read env vars or whatever else I believe, at least for the JS part.
o
👀 1
v
Well, if it is only about the JVM-part, use system properties instead and set them through a
jvmArgumentProvider
:-)
o
Actually, I'm doing both (see above issue).
For JS, I hope to be able to inject code that would pass these arguments, even if Karma is used to relay them to a browser process.