nokeedev / gradle-native

The home of anything about Gradle support for natively compiled languages
https://nokee.dev
Apache License 2.0
47 stars 8 forks source link

Create universal model wrapper to TaskContainer/ConfigurationContainer/other container #493

Open lacasseio opened 2 years ago

lacasseio commented 2 years ago

There are three things that we need to support:

  1. Support a proper whenElementKnown hook. The internal container hook of the same name in vanilla container APIs is quite broken. For this reason, we need to establish three distinct hooks to catch all possible entries from both vanilla and universal model and properly execute a whenElementKnown hook.
  2. Support vanilla Gradle API together with the universal model. We need to make the best effort so the vanilla container APIs and universal model APIs behave very similarly. According to our experimentation, eagerly created elements will be a big issue and we won't be able to safely use them. The configuration action ordering becomes very messy, very quickly.
  3. Support discovery/realization hooks. In the universal model, we can catch the realization of the domain object and perform the adequate steps for a successful, ordered, realization. In the vanilla container, we can only catch the realization once the creating provider is executing which we then need to execute additional steps. During those steps, we can cause further configuration of the domain object which, unfortunately, will be out-of-order without a creative solution. The main reason for a creative solution boils down to this line.

The first hook from number 1 is whenElementKnown which works only if the element is registered via register API. Note that the whenElementKnown action is executed twice (once on the register and once on the element realization). On a create API call, the whenElementKnown action is executed once after all configureEach actions. The second hook is very early (if possible the first configuration) configureEach to catch any create API call (as mentioned before, in this scenario, the actions are called too late). The final hook is in our register API which will properly detect when an element is known. An additional hook can be used if we use standardized NamedDomainObjectFactory (only for ad-hoc ExtensiblePolymorphicDomainObjectContainer or NamedDomainObjectContainer) which is a better replacement to the first hook and avoid issue noted in point number 3.

For discovery/realization hooks (point number 3), out-of-order configuration is an issue.We can avoid the issue if users uses our generated NamedDomainObjectProvider/TaskProvider which we can intercept the configure API and bounce the configuration via the whenElementKnown configuration delaying the configuration to the end. Another clever alternative is to disalign configuration action but that would execute configuration action in a strange way that may lead to hard to debug code as well as create a disconnect between configuration action registered via vanilla model vs universal model. The following snippet shows how to bounce the action via whenElementKnown:

void realizeBlah() {
  def a3 = { println 'a3' } as Action
  tasks.whenElementKnown(new Action() {
    boolean executed = false
    int count = 0
    void execute(def e) {
        if (e.name == 'blah') {
           count++
        }
        if (!executed && e.name == 'blah' && count == 3) {
            executed = true
            a3.execute(tasks.getByName(e.name))
        }
    }
  })
}

tasks.configureEach { realizeBlah() }
tasks.configureEach { println "a1" }
def t = tasks.register('blah') { println "a2" }
t.configure { println "a4" }

The expected ordering is a1, a2, a4 then a3. Note that a3 would normally be nested configuration only discoverable once the parent domain objects are realized. There is still an issue with ordering in a case like the following:

tasks.configureEach(CCompile) { ... } // <1>
library {
  tasks.configureEach(CCompile) {...} // <2>
}
tasks.configureEach(CCompile) { ... } // <3>

Assuming the library closure is deferred then action 1 and 3 would execute before action 2 as action 2 would only be known once library is realized. The solution here is outside of the scope of this issue but would involve reordering the action based on some global counter for configuration action. we could see that library closure would be noted as 2 which would lead the nested action noted as 2.1 (the first action under action 2 from the parent layer. The configureEach action would then be 1, 3, and 2.1. By reordering the actions before execution, we would correctly have 1, 2.1, and 3. We can see that such accuracy under these scenario is only achievable if and only if we have fully visibility on the configuration actions as well as all domain object are lazily registered, thus disallowing create APIs.

lacasseio commented 2 years ago

Not that we count 3 callbacks to whenElementKnown, once when we add the hook and 2 more times as part of the normal workflow of that API when registered.