On 06/05/2012, at 3:13 PM, Adam Murdoch wrote:

> On 06/05/2012, at 8:06 AM, Luke Daley wrote:
> 
>> 
>> On 30/04/2012, at 6:06 PM, Adam Murdoch wrote:
>> 
>>> Hi,
>>> 
>>> Something we want to do soon is to replace the buildSrc project with a 
>>> regular project. There are a few motivations for this:
>>> 
>>> * To improve the user experience for those builds that need dedicated build 
>>> logic. For example, currently the buildSrc project's 'build' target is 
>>> used. But this runs all the tests and checks, whereas for 95% of the time, 
>>> the user is only interested in compiling the classes. Or, currently we need 
>>> to clean the buildSrc project when the Gradle version changes, whereas for 
>>> regular projects we don't need to do this. Or, currently the buildSrc 
>>> project does not end up in the IDE model, but would be included if it were 
>>> a regular project.
>>> 
>>> * To allow build logic to both be published and used in the same build (but 
>>> not in the same project, for now). This will mean that you can use your 
>>> enterprise plugins in the same build that produces them. For example, you 
>>> can use your custom release plugin to release your custom release plugin. 
>>> We may use this in Gradle, too, when we add a plugin dev plugin.
>>> 
>>> * To detangle project configuration from the project hierarchy. In 
>>> particular, this required for parallel execution, so that projects can be 
>>> configured in an arbitrary order, and across multiple JVMs and/or threads.
>> 
>> There's another kind of related case that I know of. Wanting to use an 
>> artifact produced by a module (not necessarily “build logic”) in another 
>> build. Not sure it's particularly relevant to the buildSrc discussion, but 
>> it's in the same kind of space.
>> 
>> The case I had was this
>> 
>>  I built a small library for reflectively invoking Grails applications in a 
>> version agnostic manner, called grails-launcher. One of the things that this 
>> library provided was some information on what dependencies were necessary 
>> for a given Grails version and some quirks. I wanted to add a module that 
>> was effectively a test suite for launching a bunch of different Grails 
>> versions. I needed to use classes from grails-launcher in the build script 
>> to tweak the setup for each version. There's no way to do this right now 
>> with Gradle. I had to make the test project a different build.
>> 
>> What I would have liked to happen is that Gradle would have been able to 
>> realise that before it could configure the test project it would have to 
>> build the “main” project fully first, and make it available to the test 
>> project. Quite challenging.
>> 
>> The general pattern here is just wanting to use application code within the 
>> build, which I think is commonly desirable. 
>> 
>> 
>>> DSL-wise, there are 3 main use cases:
>>> 1. Declare that a given script depends on the build logic from some a 
>>> project.
>>> 2. Declare that every script depends on the build logic from some project. 
>>> Or there might be a convention for this, so that you give a project a 
>>> particular name or put it in a particular directory, and it is 
>>> automatically picked up as a build logic project.
>>> 3. Inject configuration to all projects, including those projects that are 
>>> built during configuration time.
>>> 
>>> Use case 1
>>> -------------
>>> 
>>> I think this is as simple as being able to add project dependencies to the 
>>> build script's classpath configuration:
>>> 
>>> buildscript {
>>>     dependencies { classpath project(':buildLogic') }
>>> }
>>> 
>>> When we simplify the DSL for applying plugins, this might become something 
>>> like:
>>> 
>>> apply project: ':buildLogic', plugin: 'my-custom-plugin'
>>> 
>>> Implementation-wise, the configuration phase would look something like this:
>>> 
>>> 1. Queue up the configuration of each project, in parent-first order (like 
>>> we do now).
>>> 2. For each project, if not already configured, then execute the project's 
>>> build script.
>>> 3. For each script that is executed:
>>>     * Execute the buildscript { } section of the build script.
>>>     * For each project dependency in the build script classpath, 
>>> recursively configure and build the target project. Fail if the target 
>>> project is currently being configured.
>>>     * Resolve the build script classpath and execute the script.
>>>     * For each call to evaluationDependsOn(), recursively configure the 
>>> target project. Fail if the target project is currently being configured.
>>> 4. For each project that is built during configuration:
>>>     * Configure the project as above
>>>     * For each project dependency required to build the project, 
>>> recursively configure the target project. Fail if the target project is 
>>> currently being configured.
>>>     * Add the tasks that build the runtime class path for the project to 
>>> the DAG.
>>>     * Execute the tasks.
>>> 
>>> I think this boils down to some changes to dependency resolution:
>>> 
>>> During the configuration of a project:
>>> 1. When a Configuration is resolved, for each project dependency we trigger 
>>> configuration of the target project and building of its artefacts.
>>> 2. When a Configuration's buildDependencies are queried, for each project 
>>> dependency we trigger configuration of the target project.
>>> 
>>> At other times (e.g. task execution):
>>> 1. When a Configuration is resolved, for each project dependency assert 
>>> that the target project has been configured and the artefacts built. It's 
>>> an error if not.
>>> 2. When a Configuration's buildDependencies are queried, for each project 
>>> dependency assert that the target project has been configured. It's an 
>>> error if not.
>>> 
>>> And the same kind of thing for task dependencies:
>>> 
>>> * When a task's dependencies are resolved during configuration, trigger the 
>>> configuration of the target project.
>>> * When a task's dependencies are resolved at other times, assert that the 
>>> target project has been configured.
>>> 
>>> 
>>> Some open issues:
>>> 
>>> * Currently, the buildSrc classes are available in the settings script. 
>>> This would not be the case if a regular project is used. Some possible 
>>> solutions:
>>>   - Use an external script for any shared logic.
>>>   - Allow the settings script to add projects in it's settingsscript { } 
>>> section, and resolve configurations as above.
>>>   - Move the logic to an external project, and allow plugins to be applied 
>>> to the Settings object.
>>>   - Allow build scripts to add projects.
>>>   - Chop your settings script into 2: one which defines the build logic 
>>> projects, and a second one that declares a dependency on that project and 
>>> uses it to define the remaining projects.
>>> 
>>> * Tasks can be executed before the DAG is fully populated, and before the 
>>> 'DAG ready' event has been fired. This means that some conditional 
>>> configuration may not have been executed when these tasks are executed. 
>>> Introducing build types might be an option here, so that the conditional 
>>> stuff is applied much earlier in the configuration phase.
>>> 
>>> * Projects can be configured and tasks executed before the parent project 
>>> has had a chance to do configuration injection. More on this below.
>>> 
>>> Use case 2
>>> ------------
>>> 
>>> I like the idea behind the buildSrc project: you just put your build logic 
>>> in a certain place, and it is just made available. It would be a shame to 
>>> lose this. I wonder, however, if we really need this, assuming we can 
>>> reduce the boilerplate for adding a project dependency to a build script 
>>> classpath down to a single statement. We might also tackle this by making 
>>> script 'plugins' work more like plugins, so that something like:
>>> 
>>> apply plugin: 'my-plugin' 
>>> 
>>> might come from a compiled class from another project, or might apply 
>>> $rootDir/gradle/my-plugin.gradle (or whatever).
>>> 
>>> This way, plugins are provided by the environment and the consuming script 
>>> doesn't care where they come from. What is currently in buildSrc would turn 
>>> into one of the following:
>>> * A regular project in some external build, with plugins published to a 
>>> repository.
>>> * A regular project in the same build, with plugins built locally.
>>> * A script in some conventional (or declared) location.
>>> 
>>> 
>>> Use case 3
>>> ------------
>>> 
>>> The current approach of using allprojects {} and friends for configuration 
>>> injection isn't going to work, as the build logic project will potentially 
>>> have been configured and built before the injecting script has a chance to 
>>> execute.
>>> 
>>> There are a couple of existing approaches that would work (but are a bit 
>>> awkward):
>>> * Move the shared logic to a script, and apply it from various locations
>>> * Move the shared logic to a plugin in a second build logic project, and 
>>> depend on it from various locations.
>>> 
>>> The existing configuration injection methods have some other problems. 
>>> First, these methods guarantee that the code is called for every project, 
>>> and that every project is configured. However, this stops us doing some 
>>> things:
>>> * Skip the configuration of projects that aren't relevant to the current 
>>> build. Eg in the Gradle build, don't configure all the plugin projects if 
>>> I'm running the unit tests for core.
>>> * Short-circuit the configuration of projects whose outputs are up to date. 
>>> Eg in the Gradle build, when I'm working on the c++ plugin, don't configure 
>>> all the core projects when none of their source or configuration has 
>>> changed.
>>> * Use compatible pre-built artefacts from a binary repository, rather than 
>>> configuring the projects and building their artefacts. Eg in the Gradle 
>>> build, when I'm working on the c++ plugin, just get the rest of the 
>>> binaries from the CI server (not a great example, but you get the idea).
>>> 
>>> Second, these methods guarantee that the code is always called in the same 
>>> context. This stops us doing some of these things:
>>> * Building separate chunks of the model concurrently.
>>> * Building the model across multiple JVMs or machines.
>>> 
>>> So, I think we need a new DSL here. Some options:
>>> 
>>> 1. Just change the injection methods, so that they drop these guarantees.
>>> 2. Change the injection methods so that they have 2 modes. Allow a build 
>>> script to declare which mode it needs.
>>> 3. Add new injection methods, with different names to the existing methods.
>>> 4. Use scripts in conventional locations. So, perhaps 
>>> $rootDir/gradle/allprojects.gradle is applied to each project before it is 
>>> configured.
>>> 5. Allow configuration to be injected from the settings script (with the 
>>> new semantics).
>>> 6. Add a new type of build script, with injection methods that have the 
>>> same names as the existing ones, but with the new semantics.
>>> 
>>> Option 1) is not really an option. Options 2), 3) and 6) don't solve the 
>>> build logic project problem. Personally, I like 5), because it detangles 
>>> the build configuration from the root project. What is interesting about 
>>> this option is that it allows you to have a single .gradle file for an 
>>> entire multi-project build, that both defines the projects and injects 
>>> configuration into them.
>>> 
>>> An open issue is exactly what the semantics of the injection methods would 
>>> be. They're going to have to deal with the fact that the configuration code 
>>> may end up running in various different JVMs. This has some implications as 
>>> to how values are shared across projects, e.g. a calculated version.
>>> 
>>> 
>>> Migration
>>> ----------
>>> 
>>> I think eventually we want to get rid of buildSrc altogether. The plan 
>>> would be to implement the above use cases as experimental features, leaving 
>>> buildSrc alone. Then, we should shake out the new configuration mechanism 
>>> further with some of the parallel execution and partial configuration 
>>> features. Once we're fairly happy with how this looks, we would deprecate 
>>> the buildSrc project, and later remove it.
>> 
>> I'm not sure this is a good next direction. I think we should be focussing 
>> on “the new configuration mechanism” first, and then leverage it to meet 
>> these use cases. The buildSrc project as it stands has some shortcomings for 
>> sure, but they aren't killing us. 
>> 
>> A lot of the issues you raise above are really more about “the new 
>> configuration mechanism” than the buildSrc issue to my eyes, and mixing the 
>> two will make things more complicated. If we can independently configure and 
>> build projects that are part of a multi module build then we will already 
>> have a lot of what we would need to use other projects in the classpath of 
>> other projects. I'd much rather focus on getting this right, focussing on 
>> the more common case of traditional “project dependencies”, than solve the 
>> buildSrc problem.
> 
> Perhaps I'm misunderstanding what you mean, but to me these are the same 
> thing. The idea is that when you resolve a project dependency, Gradle makes 
> sure that the target project has been configured and the necessary artefacts 
> built, regardless of where or when you need the artefacts, be that to compile 
> your build script, configure a project, or execute a task. The buildSrc 
> project is just a concrete use case that we can use to drive this, and to fix 
> some bugs and usability issues along the way. But it also happens to solve 
> the more general problem of using project artefacts at configuration time.
> 
> The buildSrc project is also a concrete use case that drives a nice subset of 
> the more general configuration problem: we need to deal with the fact that 
> configuration ordering will change based on project dependencies, but we 
> don't yet have to deal with things like concurrent execution, logging, 
> isolation, configuration inputs and outputs, caching the model, and so on. 
> These will happen in later increments (but we do need to consider them when 
> we design the solution).
> 
> The main problem with allowing the configuration order to change is that 
> existing configuration mechanisms like allprojects { } stop working, so we 
> need to address configuration to some degree, whether that's a warning when 
> you attempt to configure a project that has already been configured and 
> built, or a new DSL, or whatever.

I think my reluctance stems from using a secondary use case to drive the work, 
potentially leading to a short lived solution. This is a rather intangible 
argument though, so I'll withdraw it.

> 
> Given that using project artefacts at configuration time is a use case that 
> hasn't been supported so far, so we have the opportunity to evolve an 
> experimental DSL for a while, before we make it the preferred configuration 
> DSL and start encouraging people to use it for all configuration. So, the 
> sequence would be something like:
> 1. Add support for using project dependencies at configuration time, 
> documented as an experimental feature [1].
> 2. Remove the experimental tag. Update samples and user guide to prefer using 
> project dependencies over buildSrc.
> 3. Deprecate buildSrc.
> 4. Remove buildSrc.
> 
> Of course, each step will be separated by a decent period of time (e.g. 4. 
> cannot happen until Gradle 3.0 at the earliest).
> 
> [1] 'experimental' isn't the right term here, but we don't have a good term 
> for this yet.
> 
> 
>>  I think we really need to look at publications and project dependencies (as 
>> they are implemented now) before we can go down “the new configuration 
>> mechanism” route. 
> 
> This is happening too. I do think it's somewhat orthogonal to the 
> configuration model, in that we can tackle one without tackling the other. 
> And we can certainly go parallel with the existing dependency model, but we 
> definitely cannot go parallel with the existing configuration model.
> 
> We'll be using use cases from c++, and the IDE world, and maybe OSGi, to 
> flesh out a dependency model that allows us to solve things like partial 
> configuration, short-circuiting project configuration, build aggregation, and 
> so on. 

And not putting runtime dependencies of project dependencies on the compile 
classpath too.

-- 
Luke Daley
Principal Engineer, Gradleware 
http://gradleware.com

Reply via email to