Open lptr opened 9 years ago
daf2e0b fixes the first problem by marking the configuration as modified when a parent configuration is modified in a non-lenient way.
Still not sure how to solve the second problem, though.
One way of dealing with the second problem is to introduce a beforeObserved()
listener, and do such business there.
The problem is, if we exposed beforeObserved()
, it would make sense to expose the OBSERVED
state on the public API as well.
There is an implementation for this in f219a85. It breaks compatibility, though. :(
Thanks for your deep analysis. It really helps clarify the problem. I think that the behaviour of 'beforeResolve' is currently broken, at the very least unhelpful.
In Gradle, we're using 'beforeResolve' to set up a default dependency if none is explicitly added. I'd expect any extending configurations to have these default dependencies configured as well.
So if "compile extends antlr" then I think it would be more helpful if 'beforeResolve' hooks on antlr were fired before 'compile' is resolved: this would mean that we could replace the 'beforeResolve' implementation with the 'beforeObserved' hook that you've added (and not add any new API). This would be a 'potentially breaking change' but in many respects a bug that is now fixed. We are changing the meaning from "before explicitly resolved" to "before participates in resolution".
Does that sound like it would work?
Benefit: beforeResolve can be used to predictably modify a parent configuration, rather than the end result being dependent on order that configurations are resolved Problem: it would be hard to guarantee that 'afterResolve' is fired symmetrically with 'beforeResolve', since 'beforeResolve' would be fired on any usage in resolution, but afterResolve would need to wait until explicity resolved. I'm not sure if this is really a problem.
I think that would be great. I explored that route first, too, and bumped into the requirement that beforeResolve()
and afterResolve()
should be called symmetrically. It seems that we either need to break that symmetry, or break the API by introducing a new event. My guess is that breaking that symmetry would be less of a problem, but I haven't yet looked into it.
The basic problem is that using beforeResolve() has never worked as a reliable way to add defaults. I'd just add a way to provide defaults when the configuration is empty.
We should not change the semantics of beforeResolve() and afterResolve().
If we fix the order of calling beforeObserve()
, that could serve as a place. Maybe calling this something that doesn't include "observe" in its name would make it look less like exposing internals.
Or do you plan a separate DSL just for this? What was the problem with using beforeResolve()
before?
I did some deeper investigation. It's possible to make this work by using beforeObserve()
. We can still introduce a new DSL for declaring default dependencies, and it can work via beforeObserve()
(which can remain internal).
To do this, the order of calls on a Configuration
must look like this:
beforeObserve()
, but do not set state = OBSERVED
-- potentially adding more dependenciesstate = OBSERVED
here eitherWe must only set state = OBSERVED
on visited configurations after the traversal has finished. Otherwise we'd be emitting warnings about modifying an observed configuration when a dependency configuration adds a default dependency upon being observed.
This means we need some way to collect visited configurations during traversal.
*
There's another approach to supporting a new DSL to specify a default dependency. This would rely on a configuration to be able to return its default dependencies when there are no other dependencies declared.
Something ugly like this:
public DependencySet getAllDependenciesOrFallBackToDefaults() {
if (allDependencies.isEmpty()) {
return allDependencies;
} else {
return defaultDependencies;
}
}
I like it a lot that this solution would not depend on last-minute mutation. One downside is that we couldn't really call dependency listeners like all()
in any meaningful way. Not sure if this is a problem, though. After all, getAllDependencies()
only returns the default dependencies now after the configuration has been resolved, which is a bit confusing already.
This solution would need a simpler implementation, and we could remove beforeObserve()
completely. And this would also enable us to mark configurations as observed during any point in the traversal of the graph, because there would be no mutation as such.
Some other questions about a new DSL for adding default dependencies:
A couple more questions about how this new DSL should work:
allDependencies.isEmpty()
makes more sense from the user's standpoint, but I could argue either way.@adammurdoch, @bigdaz, can you please comment? Thanks.
I would do something like this:
whenEmpty(Action<? super DependencySet>)
method to Configuration
Other comments:
What should happen to all the places where we rely on getDependencies()
/getAllDependencies()
? In many places it would make sense to process default dependencies, too. Shall we run whenEmpty()
from getAllDependencies()
perhaps?
If not, what happens to code that wants to process all dependencies, even default ones? Like these:
(In some cases the best solution would be to rely on ResolutionResult
if they want to process true dependencies, but still.)
Would introducing getEffectiveDependencies()
solve anything?
IIUC, default dependencies added via beforeResolve()
were not always processed in these places, as the presence of default dependencies depended on whether or not the Configuration
had already been resolved or not. That said, I don't think we should leave it like that.
We'll also need to figure out how to solve ShortcircuitEmptyConfigsArtifactDependencyResolver
. Previously, beforeResolve()
was already called when we got here, but now we don't know if whenEmpty
was already called here:
Here's a very simple implementation that doesn't solve any of the previous problems: https://github.com/gradle/gradle/pull/424
The
antlr
plugin uses a trick to add a default ANTLR dependency if no other was given:There are two problems here:
compile
configuration (that extendsantlr
) for task dependencies, it's set tomodified = false
. Whenantlr
gets a new dependency, thecompile
configuration isn't notified about the change, and when we ask it to resolve for results, it simply does not re-resolve.compile
for task dependencies, theantlr
parent configuration gets observed. When we resolve theantlr
configuration for results (and add the dependency inbeforeResolve()
), it's already inOBSERVED
state, and we inevitably print a deprecation warning.