If there is a thing in Gradle that regularly confu...
# community-support
y
If there is a thing in Gradle that regularly confuses me, it is rolling custom artifacts for consumption by the root project or other subprojects.
Copy code
// Assume that there is a task with a single output property 
// and signature Provider<File> getOutputFile().
// just calling it 'task' below instead of the whole task.named(....)

configurations.create('myCustom') {
  canBeResolved = false
  canBeConsumed = true
  attributes { /* do some custom attrs */ }

  outgoing {
    // Option 1
    artifact(task.flatMap { t -> t.outputFile } ) { cpa ->
      cpa.builtBy(task)
    }
    // Option 2
    artifact(task)
  }
}

// or even option 3
project.artifacts.add('myCustom',task)
When that is linked to another subproject
Copy code
// assume two configurations myCustomIncoming, myCustomResolvable where the latter extends the former, but is resolvable

dependencies {
  myCustomIncoming project(path: ':sub-project-with-my-custom', configuration: 'myCustom')
}
When I resolve
myCustomResolvable
, I would expect the task from earlier to be executed in order to create the appropriate artifact, but it doesn't. (it knows what the artifacts path is, it just does not execute the task beforehand). I would expect this to be relatively straight forward, but sometimes I end chasing my tail in getting something like this to work. Anyone else jumping the same kind of hurdles?
p
I use option 3, but did you also add the attributes to the resolvable configuration? And you should avoid specifying the target configuration in the dependency
y
@Philip W Yes, attributes were done.
v
Attributes should be irrelevant, shouldn't they? I mean when requesting a specific configuration, then that should always be used and no attribute-matching done. Also, can you share a complete MCVE? Completing your snippet as
Copy code
val task by tasks.registering {
    val output = layout.buildDirectory.file("foo.txt")
    outputs.file(output)
    doLast {
        output.get().asFile.writeText("foo")
    }
}
configurations.create("myCustom") {
    isCanBeResolved = false
    isCanBeConsumed = true

    outgoing {
        artifact(task)
    }
}
and
Copy code
val myCustomIncoming = configurations.dependencyScope("myCustomIncoming")
val myCustomResolvable = configurations.resolvable("myCustomResolvable") {
    extendsFrom(myCustomIncoming.get())
}

dependencies {
    myCustomIncoming(project(path = ":foo", configuration = "myCustom"))
}
val foo by tasks.registering {
    inputs.files(myCustomResolvable)
    doLast {
        myCustomResolvable.get().files.forEach { println(it) }
    }
}
works fine here.
y
@Vampire Yes, attributes serves as a filter, and if not supplied then all files should be available. BTW
inputs.files(myCustomResolvable)
is a trick. It is not obvious that it should be done and I don't think it is anywhere in the docs. I have fallen into trap at least once, where I was using providers in the task, but nothing worked until I added the configuration as an input.
v
Yes, attributes serves as a filter, and if not supplied then all files should be available.
Attributes serve as filter if you request a specific configuration explicitly? o_O Wasn't aware of that, I thought if you request by configuration explicitly you get that configuration, if you do not specify a configuration explicitly, then attribute- / variant-aware resolution is used.
BTW
inputs.files(myCustomResolvable)
is a trick.
It is not so much a trick imho. It is just declaring the task inputs via ad-hoc API as I did not use a proper task implementation with configured input properties. Using a proper task implementation like this also works fine:
Copy code
dependencies {
    myCustomIncoming(project(path = ":foo", configuration = "myCustom"))
}
abstract class Foo : DefaultTask() {
    @get:InputFiles
    abstract val input: ConfigurableFileCollection

    @TaskAction
    fun foo() {
        input.files.forEach { println(it) }
    }
}
val foo by tasks.registering(Foo::class) {
    input.from(myCustomResolvable)
}
y
@Vampire whichever way, it still needs to be an input.
v
Of course, tasks always should declare their inputs and outputs properly for proper operation. 🙂
y
@Vampire Maybe I should explain the occasional hole I have fallen into in more detail. let's say that in the consuming project I have task with the following input.
Copy code
@Input
Provider<String> getSomeContent()
and that provider is something like
Copy code
final FileCollection incomingFiles = project.configurations['myCustomIncoming']
// just assume one file for simplicity
project.provider { -> incomingFiles.singleFile.text }
that won't invoke the task in the producing subproject when the consuming task's properties are serialized. and the way to get it to work is to add
Copy code
inputs.files(incomingFiles)
once you know that and retrospectively, you understand why, but this is not straight-forward and clear when wiring up something complex for the first time.
v
Well, yeah, that's always the problem when you use inputs that you do not declare and use providers that do not carry task dependencies. In my example the idiomatic way would then be
Copy code
abstract class Foo : DefaultTask() {
    @get:Input
    abstract val input: Property<String>

    @TaskAction
    fun foo() {
        println(input.get())
    }
}
val foo by tasks.registering(Foo::class) {
    input = myCustomResolvable.flatMap { it.elements }.map { it.single().asFile.readText() }
}