Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Identify and promote first-class support for mapping between extension and task properties #726

Closed
eriwen opened this issue Oct 19, 2016 · 15 comments
Labels
a:epic in:configuration-model lazy api, domain object container stale

Comments

@eriwen
Copy link
Contributor

eriwen commented Oct 19, 2016

Plugin developers are currently using the internal API for convention mapping to map extension values to task properties. Convention mapping is the only effective method to avoid evaluation order issues for this use case.

We need to identify a first-class, public API solution for the problem. The software model was supposed to solve the issue. As we won't promote the software model for the JVM domain, we'll need a different solution. Part of this ticket is to identify an appropriate solution. This could be convention mapping but doesn't have to.

If we go with convention mapping then we should make the API public. We should also tweak the contract to make the value immutable when the task starts executing.

@eriwen eriwen added the a:feature A new functionality label Oct 19, 2016
@eriwen eriwen added this to the 3.3 milestone Oct 24, 2016
@ajoberstar
Copy link
Contributor

As we won't promote the software model for the JVM domain, we'll need a different solution.

I'm hoping you mean "won't promote it yet"? Just want to make sure software model is still the direction.

@philmcardle
Copy link

Apologies, I'm 100% sure this isn't the appropriate place to ask this, but it's an open question for us too - was @ajoberstar's question answered anywhere?

The radical differences between the JVM domain and the software model are a cause of near-constant complexity for someone using Gradle for native, so it would be good to know which way Gradle is going.

@eriwen eriwen removed this from the 3.3 milestone Nov 28, 2016
@bmuschko bmuschko self-assigned this Jan 16, 2017
@bmuschko
Copy link
Contributor

I added a spec for this work. Feedback welcome!

@ajoberstar This work won't touch the software model. Convention mapping has been treated almost like a public API by plugin developers in the past years. I have done it, I am sure you have done it. There's a good reason for it: there were simply no better options. With the software model you can effectively avoid the evaluation order issue (addressed by convention mapping) by defining model rules.

@ajoberstar
Copy link
Contributor

@bmuschko That makes sense to me, and I like the focus on getting some issues to round out the public APIs. My original question was more of an aside, and I don't know that it's quite been answered. Since this came up recently on the forum too, maybe that's a better place to continue if you don't mind taking a look.

@bmuschko
Copy link
Contributor

@ajoberstar Thanks for raising the discussion. It's a valid concern for any plugin developer. Let's continue the discussion in the forum.

@adammurdoch
Copy link
Member

adammurdoch commented Feb 14, 2017

I think there are a few options for making the 'provider' approach more convenient.

We can make some progress towards a more managed model and remove a bunch of boilerplate by allowing Gradle to manage the state for a property and to provide getter and setter implementations.

For example:

abstract class MyTask extends DefaultTask {
    @Input
    abstract String getSomeProp();
    abstract void setSomeProp(String value);
    abstract void setSomeProp(Provider<String> value);
}

Gradle can provide implementations for these methods. The current decoration that mixes in convention mapping basically already does this.

For backwards compatibility, the generated methods would still be convention mapping aware. So you can still do myTask.conventMapping.someProp = { "123" } so that myTask.someProp == "123".

No task types are currently abstract but can be extended. So it would be a breaking change to add abstract methods to an existing task type or to add abstract to existing task methods. We could use the approach we use to inject services via a getter, so that the task defines a stub method that is later overridden by the decoration. The downside of this approach is that a call to super.getSomeProp() in a task subtype will not be intercepted. Another option is that these methods might just delegate to conveniences provided by DefaultTask, and we change them to abstract methods in a major release.

We might get rid of the need for an additional setter that accepts Provider<T> by including an API on task that allows you to set a property value given a property name and a Provider:

abstract class MyTask extends DefaultTask {
    @Input
    abstract String getSomeProp();
    abstract void setSomeProp(String value);
}

// statically typed language
myTask.setProperty("someProp", someProvider);

// dynamically typed language
myTask.someProp = someProvider;

The point here is to avoid mixing a bunch of different concerns into the model (the task type).

For nested data structures, we should provide some kind of 'create a decorated instance of this type' feature.

One option would be to add a public API for Instantiator, so that the constructor for a class can be injected with an Instantiator and create whatever nested objects it needs, calling the setters to attach this. This, along with the managed state from above, would be applied recursively so that the desired object graph can be assembled.

Another, not mutually exclusive option, would be for Gradle to automatically create instances of things that it knows how to create: collection types, DomainObjectContainer types, types with an @Inject constructor, interfaces, etc.

@adammurdoch
Copy link
Member

adammurdoch commented Feb 14, 2017

Digging a little further into the cases where abstract methods cannot be used:

The first option might look something like this:

class MyTask extends DefaultTask {
    @Input @Inject
    String getSomeProp() { throw new UnsupportedOperationException(); }
    @Inject
    void setSomeProp(String value) { throw new UnsupportedOperationException(); }
}

Gradle would override the getter and setter, as it does now, but would also manage the state of the property instead of delegating to the getter and setter.

However, this would be broken:

class MyCustomTask extends MyTask {
    void setSomeProp(String value) {
        value = ... some calculation ...
        super.setSomeProp(value);
   }
}

The second option might look something like this:

interface StateHolder<T> extends Provider<T> {
    void set(T value); // uses the provided value
    void set(Provider<? extends T> provider); // delegates to the provider
}

class MyTask extends DefaultTask {
    // an implementation is provided by Gradle and injected
    @Inject
    private final StateHolder<String> someProp;

    @Input
    String getSomeProp() { return someProp.get(); }

    // setters delegate to methods on the injected value
    void setSomeProp(String value) { someProp.set(value); }
    void setSomeProp(Provider<String> provider) { someProp.set(provider); }
}

There'd be some integration with the convention mapping infrastructure for backwards compatibility. We know which state object or field is associated with which property in both cases.

@adammurdoch
Copy link
Member

Nested objects might work something like this:

abstract class SomeReport {
    abstract String getSomeProp();
    abstract void setSomeProp(String value);
}

// Using an instantiator
abstract class MyTask extends DefaultTask {
    private final SomeReport report;

    @Inject
    MyTask(Instantiator instantiator) {
        report = instantiator.newInstance(SomeReport.class);
    }

    @Nested
    public SomeReport getReport() { return report; }
}

// Injected via a field
abstract class MyTask extends DefaultTask {
    @Inject
    private final SomeReport report;

    @Nested
    SomeReport getReport() { return report; }
}

// Injected via a getter
abstract class MyTask extends DefaultTask {
    @Inject @Nested
    abstract SomeReport getReport();
}

A task might also want to hide the implementation type of a nested object:

// Using an instantiator
abstract class MyTask extends DefaultTask {
    private final SomeReportInternal report;

    @Inject
    MyTask(Instantiator instantiator) {
        report = instantiator.newInstance(SomeReportInteral.class);
    }

    @Nested
    public SomeReport getReport() { return report; }
}

// Injected via a field
abstract class MyTask extends DefaultTask {
    @Inject
    private final SomeReportInternal report;

    @Nested
    SomeReport getReport() { return report; }
}

// Injected via a getter
abstract class MyTask extends DefaultTask {
    @Inject @Nested
    abstract SomeReportInternal getReportInternal(); // would need to be package protected

    public SomeReport getReport() { return getReportInternal(); }
}

@adammurdoch
Copy link
Member

Something to note is that these capabilities would not be specific to Task subtypes, but to any decorated type. In particular, this would mean that they are available for extension types.

@adammurdoch
Copy link
Member

For unit testing these types, I think ProjectBuilder would work fine:

def project= ProjectBuilder.build()
def myTask = project.createTask("myTask", MyTask);

// do stuff

@bmuschko
Copy link
Contributor

Thanks for your feedback, @adammurdoch.

Based on the feedback I got from other team members and you it seems like we feel comfortable with moving forward with the provider approach. Do you agree?

Your comments above focus on the convenience aspect of the implementation. I am wondering if we could just release the provider implementation in multiple phases. Phase 1 would not provide the aspect of convenience. I am proposing the approach for two reasons:

  • Delivering a public API that solves the problem of providing lazily evaluated values will instantly make the life of plugin developers so much better. We'd also be able to get feedback from end users.
  • I often times hear that users of Gradle would like to see less magic behavior. Generating getters/setter removes boilerplate code but it can also confuse users. Some people would rather be under full control than let Gradle sprinkle magic.

In practice I do not see people mixing convention mapping and provider approach in their plugins. I'd expect external plugins to fully switch over to the new approach and release a new version. The interoperability aspect is probably more useful for internal plugins which will migrate to the provider approach over time.

You also mentioned making the Instantiator a public API. That's definitely planned but as a separate story: #728.

@adammurdoch
Copy link
Member

Yeah, I agree. We could (and should) certainly do this in stages.

I'm not seeing generated getters and setters as magic. It's pretty explicit that someone else has to provide the implementation. In this regard it's quite a bit better than convention mapping.

At this stage generated getters and setters are an important part of the performance strategy, as they allow us to cache state, reset state and make things immutable or parallel safe. Probably also part of the software model migration story as well. This pattern is intended to become pervasive in all domain object types.

In practice I do not see people mixing convention mapping and provider approach in their plugins

This would instead be important for people that reuse tasks types from other plugins, or who add or override mappings for tasks created by other plugins. Interop with convention mapping means that the plugin author can switch styles without affecting downstream users. And downstream users can switch styles without being blocked on the plugin author releasing a new version.

@bmuschko
Copy link
Contributor

PR: #1452

mkobit added a commit to mkobit/junit5 that referenced this issue Mar 27, 2017
This enables users to retrieve and add dependencies, configure inputs, and other user-supplied details at configuration time.

We need the user supplied configuration to be applied lazily after they configure it, which means we cannot do it at plugin `apply` time.
We can also use `Callable`/`Closure` in the `inputs.property` method, but that will only work for properties but not for methods.
Another way of accomplishing lazy properties is by using `conventionMapping`, but that also has pitfalls (see https://github.com/gradle/gradle/blob/master/design-docs/internal-apis-made-public.md).
This means we don't have an effective way of lazily configuring the `args` or the `systemProperty` of the task.

The easiest way forward is to simply create the task at configuration time and then apply the rest of the configuration in the `afterEvaluate` method to avoid using internal `conventionMapping`.

closes junit-team#708

See related Gradle issue for making lazy properties at gradle/gradle#726
sormuras pushed a commit to junit-team/junit5 that referenced this issue Apr 3, 2017
* Update to latest Gradle build scan plugin and change Travis CI option to first class `--scan` option
* Upgrade to Checkstyle 7.6
* Introduce Gradle functional tests for validating `'java'` and `'java-library'` plugins work
- Generate classpath manifest for the testkit test compile classpath.
  This is useful so that the locally built versions of the other projects are used during functional testing.
- Test against both the `'java'` and `'java-library'` plugins
* Add functional test for validating `UP-TO-DATE` behavior when test is executed multiple times with no changes
* Make Spock tests more standardized by using `given`
* Switch task creation to use `TaskContainer` and repetitive configuration
* Create `junitPlatformTest` task during configuration phase
See related Gradle issue for making lazy properties at gradle/gradle#726

Closes #708
@eriwen eriwen modified the milestones: 4.1 RC1, 4.2 RC1 Apr 13, 2017
@eriwen eriwen removed the a:feature A new functionality label Jul 19, 2017
@bmuschko bmuschko modified the milestones: 4.3 RC1, 4.2 RC1 Aug 31, 2017
@bmuschko bmuschko removed this from the 4.3 RC1 milestone Oct 12, 2017
@big-guy big-guy added the in:configuration-model lazy api, domain object container label Feb 27, 2018
@stale
Copy link

stale bot commented Jul 13, 2020

This issue has been automatically marked as stale because it has not had recent activity. Given the limited bandwidth of the team, it will be automatically closed if no further activity occurs. If you're interested in how we try to keep the backlog in a healthy state, please read our blog post on how we refine our backlog. If you feel this is something you could contribute, please have a look at our Contributor Guide. Thank you for your contribution.

@stale stale bot added the stale label Jul 13, 2020
@stale
Copy link

stale bot commented Aug 3, 2020

This issue has been automatically closed due to inactivity. If you can reproduce this on a recent version of Gradle or if you have a good use case for this feature, please feel free to reopen the issue with steps to reproduce, a quick explanation of your use case or a high-quality pull request.

@stale stale bot closed this as completed Aug 3, 2020
@wolfs wolfs closed this as not planned Won't fix, can't repro, duplicate, stale Sep 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a:epic in:configuration-model lazy api, domain object container stale
Projects
None yet
Development

No branches or pull requests

7 participants