John Murph wrote:
On Fri, Nov 20, 2009 at 3:53 PM, Adam Murdoch <a...@rubygrapefruit.net <mailto:a...@rubygrapefruit.net>> wrote:



        First is TestListener.  This (if you can recall that far back)
        was the reason I did the listener manager changes.  We need to
        have an init script that can register a listener that gets
        testing feedback from JUnit.  This will allow us to improve
        our Gradle TeamCity runner so that it gets better (and real
        time) test results.  I'm thinking along the lines of "if a
        listener of a certain type is registered with the listener
        manager, then give a remoting listener to JUnit that sends
        messages to an in-process peer that forwards the messages on
        to the registered listener."  Do that make sense?


    This would be nice to have.

    I suspect most of the work has been done already in the NativeTest
    task, so that the events are available just below the surface of
    the NativeTest task. We'd need to add some methods for adding a
    TestListener to NativeTest, with an impl of that used a
    ListenerManager to create its broadcaster. Then, the TestListener
    events would be available in the init script.

    I don't think we should bother with trying to fit this into the
    AntTest task as well, and should instead focus on getting the
    NativeTest stuff ready.


OK, I'll have a look at the NativeTest stuff and see what I can figure out. This is the change that is most pressing for us as we currently don't have a workaround, so hopefully I can figure something out.

If you need something really soon, it might be better to try this with AntTest. Will be a little while before we make NativeTest the official test task. It's up to you. We can figure a way to merge the two implementations if you decide to go with AntTest.


        Second is extension support.  This is the idea that a script
        can ask for some external script to be executed against a
        given delegate object.  Doing this through Gradle would allow
        for all the normal caching stuff to be reused.  We need to be
        able to run such scripts from the settings.gradle (and from
        inside build scripts, but that is obvious).  We would use this
        feature to define our own custom domain objects that are
        configured via external script files.  I'm not sure what the
        interface to Gradle should be, a method on Gradle called
        "executeExtension" that takes a file and a delegate object?
         Or maybe a "getExtensionSupport" method that returns an
        ExtensionSupport class that provides an execute method?  What
        stateful information could ExtensionSupport have that
        justifies a separate class?  Or maybe the justification is
        that it provides more than one method, like what?  I'm not
        sure what you guys might be thinking about this feature as we
        have not discussed it much before.


    Why do you need to do the configuration using external scripts?
    What do they configure? I want to get a feel for the use case
    before we discuss a solution.

We showed and explained this to Hans when he was here. It's a bit difficult to explain in an email, but let me try. We have the concept of multiple modules being put together to form products, and multiple products forming installs and multiple installs forming distributions. Each of these three levels (products, install, dists) have logic and therefore need configuration of that logic. For example, what modules does this product need? What is the name of the product? What native launchers does the product need, and what are it's settings (Xmx, etc.)? Similarly, installs and distributions have configurations as well.

To support this, we have three domain objects in our buildSrc that hold this information. We also have various "xxx_product.gradle" and "xxx_install.gradle" and "xxx_dist.gradle" files in our project that are used depending on what is being built. These special .gradle files are executed on the associated domain object by our settings.gradle. This allows our developers to only say what's important to them, and not have to see our giant build.gradle scripts (or our buildSrc module). What our developers actually think is Gradle is all these special scripts, they never see "true" Gradle.

The thing is that I want these special Gradle files to be very similar in treatment to normal Gradle files. All I'm really doing is extending the concepts that Gradle knows about into my own custom domain. (This ability is at the heart of what we love so much about Gradle.) Unfortunately, to do this right now I had to copy-paste some Gradle implementation code into our buildSrc (the stuff than runs a script, knows when/where to cache it, knows when to ignore the cache and rebuild it, knows how to "delegate" to an object, etc.) Every time you guys touch that stuff it breaks us, because we are using internal APIs of Gradle. The idea of this feature is to make an easy to use public API so that we don't get broken so much. It will also make it easier for others to do this same thing.

As a matter of fact, we've talked to Hans about our idea that large projects should generally be structured this way. I don't want to go into details right now (I intended to write up something more complete eventually), but most large projects have lots of concepts centered around what they are doing and the way they work. Formalizing those concepts into "extensions" to Gradle can product a much easier to use build engine that is designed for them. It's why I've always agreed that Gradle should be a toolkit. One that works out-of-the-box for simple projects, and one that lets large projects implement their own custom build engine on top of it. Part of the power of Gradle is that it can be both.


Thanks for writing this up. I agree 100% with you on the goal. I think this is an excellent pattern for large builds, where the 'build scripts' describe what to build in some high-level build language (custom or otherwise), and some build logic in the root script/buildSrc/some plugin takes care of how to build those things.

I have a different suggestion for how we solve this problem. To me, it feels like we want some unified theory of build logic composition. That is, these all feel like the same thing:

Things we do now:
- configure a Project using a plugin
- configure a Project using a build script
- configure a Gradle instance using an init script
- configure a Settings instance using a settings script

Things we want to be able to do:
- implement a plugin using a script
- configure a Project using an arbitrary script
- configure a Gradle instance using a plugin
- configure a SourceSet/Configuration/RepositoryContainer/etc using a plugin/extension
- configure an arbitrary domain object using an arbitrary script
- auto-configure a Project using scripts or plugins or java source in a well-know location (such as buildSrc)
- ...

So, I think we could get rid of Project.usePlugin(), and replace it with a general method. Let's call it 'configure' for now. In any script you would be able to do:

configure(object) { a closure }
- this is a generalisation of Project.configure(), available from any script

configure(object, 'path/to/a/script.gradle')
 - executes the given script with the given object as its delegate

configure(object, SomePluginClass)
- configures the given object using the given plugin, i.e. a generalisation of Project.usePlugin()

The target object would default to the delegate of the current script, ie in a build script the target object is a Project:

configure('path/to/a/script.gradle')
configure(SomePluginClass)

We could also add some additional ways to locate the build logic artifact:

configure(object, 'path/to/a/plugin.jar')
- locates the plugin(s) in the provided jar and applies them to the given object

configure(object, 'http://some.corporate.server/global-config.gradle')
 - uses the artifact (script or jar) from the given url

configure(object, 'path/to/some/directory')
- treats the directory as a Gradle project which produces a plugin jar, ie exactly the same as buildSrc

configure(object) {
   dependency 'some.org:sharedConfig:1.4+'
}
- uses the artifacts (scripts or jars) from the given dependency.


Then, our existing behaviours can be implemented in terms of configure()

- init script -> configure(gradle, initScriptPath)
- settings script -> configure(settings, settingsScriptPath)
- build script -> configure(project, buildScriptPath)
- buildSrc -> configure(gradle, "$rootDir/buildSrc")
- usePlugin('name') -> configure(project, getClassForName('name'))
- usePlugin(pluginClass) -> configure(project, pluginClass)


--
Adam Murdoch
Gradle Developer
http://www.gradle.org

Reply via email to