Hi Magnus,

On Mon, Sep 6, 2010 at 7:05 PM, Magnus Rundberget
<[email protected]>wrote:

>  Thx a lot for your swift reply.
>
> Hans:> "..expensive operations are done at configuration time which would
> be better delayed to execution time"
>
> I quickly found one by chance. We have a custom test task that replaces the
> default java plugin test task. This task is of type JavaExec.
> By moving setting of systemproperties from config to a doFirst clause I'm
> now down to approx 4 seconds.
> ... one of the systemproperties I set is using
> configurations.testRuntime.asPath, I have a feeling this was the culprit
> causing all the dependency resolution reports in the log as well :-)
>

Configuration information can have multiple roles:

1.) *Execution Set-Up:* Obviously it is needed to execute the task properly
(doFirst configuration actions work for this)

2.) *Fail-fast validation* if a task is properly configured. For example if
and only if the upload task is to be executed and the upload password is not
set, let the build fail before anything is executed. Otherwise you would for
all the unit tests and integ tests and documentation to be build and then
get the information that something is missing to successfully execute the
build. Not all validation can be fail-fast (e.g. whether some input produced
by some other task is valid). Fail-fast validation does obviously not work
with doFirst configuration.

3.) *Configuration sharing*. In Gradle we promote to avoid fake global
variables. One task should rather ask another task (or other domain objects)
for information.

task taskA {
   b = javaexecTask.classpath
}

doFirst configuration does not work here.

4.) *Configuration transformation*.

task taskA {
   b = javaexecTask.classpath.copy { dep -> // some filter }
}

doFirst configuration does not work here.

5.) doFirst configuration can also be confusing and hard to read.

The problem is that some configuration information is expensive to produce
(e.g. resolving dependencies). Specially in larger multi-project builds this
can add up to a real slow down. doFirst configurations provide a solution to
this, what are the alternatives? This depends on your use case.

*Rich lazy elements*
*
*
task main(type: Javaexec) {
   // configurations.runtime is resolved at execution time. Yet another task
can ask the main task at configuration time about its classpath can do
something with this information.
   classpath configurations.runtime
}

Rich lazy elements allow 1-4 from above and yet are inexpensive at
configuration time. For example a fail fast validation could check whether
the configuration does contain project dependencies if this is not allowed.

We love rich lazy elements. They provide a modifiable spec/rule instead of
actual values. This has the important advantage of being fully order
independent. So a plugin X can do:

add taskA
add taskB {
   b = taskA.lazyElement
}

In your build script:

apply plugin: 'X'

taskA.lazyElement.add ... // This is picked up by taskB as well.

*Non-rich lazy elements*

copy {
   // from can accept a closure. This is not as rich as the example above as
the closure is more or less a black box at configuration time.
   from { configurations.runtime.collect { zipTree(it) }
}

javaexec {
   classpath { // some logic that must return object accepted by the
Project.files method } // can also accept a cosure
}

They do not allow fail-fast validation and configuration transformation
(although the information is available at configuration time. But you would
need to pay the price of full evaluation. Not to do this is the very reason
why you are using lazy elements). But they are preferable to a doFirst
configuration, if just for readability.

*Lazy Elements*
*
*
It is not always possible to use lazy elements. It depends on the Gradle DSL
where you can use lazy elements and where not. For example we don't offer a
rich lazy element to express the common use case to configure a fat jar.
Right now you have to use a non-rich lazy element. If the from method
wouldn't accept closures you couldn't even do that.

jar {
    from { configurations.runtime.collect { zipTree(it) }
}

Gradle will have an eye on the common use cases to increase the
applicability of rich lazy elements. For the use case above
FileCollections/FileTree might provide an expand method in the future that
returns a lazily unpacking FileTree. In any case lazy elements (rich or
non-rich needs to be supported by the Tasks and/or domain objects (e.g.
FileCollection resolves lazily). If for a certain use case using lazy
elements is not possible a doFirst configuration might be the best option
(although a convention object might be another alternative, see below).

*The upcoming configuration task DSL*

This DSL will provide natural solutions for some use case where you now
would use a doFirst configuration action. But it happens at a different time
of the build. Have a look at a task that does not support lazy elements:

class A extends DefaultTask {
   Set files
}

task a(type A) {
   files = configurations.compile.files
}

With the new configuration DSL the configuration compile is only resolved if
a is executed. But it happens before any task is executed. Let's assume for
the sake of the example that the resolve takes always 10 secs and that task
a is executed after the test task. In that case you would have to wait
always 10 secs before the test are run. If everything is successful, this
does not affect the build time. But if the tests fail, it takes you 10 secs
longer then necessary to get the information. This would not be the case
with a doFirst action. But the best solution would be to replace the
Set<File> type with a ileCollection. (BTW: This scenario is of course not
why we will introduce the new configuration task DSL).

*Convention Values*
*
*
Right now you can do for any task (using the example from above):

task a(type A) {
   conventionMapping.files = { configuration.compile.files }
}

This closure will be resolved the first time the 'files' property is
accessed. So you can't use it for configuration sharing. And its syntax is
kind of funny. But it is another alternative to a doFirst configuration.

And we want to improve this. First off all we want to allow to assign a
closure to any value without the conventionMapping syntax. Just files =
{...}. This will work even if the type of files is strongly typed in the
Task definition. We could also provide a DSL syntax that allows
configuration sharing. Just to get some idea:

task b {
   c = a.conf('files') // Depending on how it was configured, returns the
closure or the actual value.
}

Sorry for hijacking your question to write a lengthy treatise on
configuration ;)

- Hans

--
Hans Dockter
Founder, Gradle
http://www.gradle.org, http://twitter.com/gradleorg
CEO, Gradle Inc. - Gradle Training, Support, Consulting
http://www.gradle.biz

Reply via email to