-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Refactor configuration creation to ensure Gradle has full visibility of Configuration's contents #25613
Refactor configuration creation to ensure Gradle has full visibility of Configuration's contents #25613
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added some commentary to highlight some potential trouble spots that the maintainers may want to consider.
...ation-plugin/src/test/java/io/quarkus/gradle/tooling/ConditionalDependenciesEnablerTest.java
Outdated
Show resolved
Hide resolved
* The platform configuration updates the PlatformImports, but since the PlatformImports don't | ||
* have a place to be stored in the project, they're stored here. The way that extensions are | ||
* tracked and conditional dependencies needs some attention, which will likely resolve this. | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is, in my opinion, revealing a possible design flaw. The comment block is here to explain the purpose, not to excuse it. I didn't feel capable of attacking the underlying data model problem without the PR becoming unwieldy to read.
if (project.getConfigurations().findByName(this.deploymentConfigurationName) == null) { | ||
project.getConfigurations().register(this.deploymentConfigurationName, configuration -> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This pattern of findByName
followed by register
feels like a code smell to me, but I couldn't find a more concise way to check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would maybeCreate
not work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would but then the registered function will be registered more than once. This class is instantiated more than once per build.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#maybeCreate
already handles potential existing one. That's the "maybe" part.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBH, I'm not sure why these get registered instead of created anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now if this deferred creation actually gained something that would be a different story. But it does not here.
That I can see anyway. Please correct me if I missed something
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now if this deferred creation actually gained something that would be a different story. But it does not here.
That I can see anyway. Please correct me if I missed something
You haven't missed anything. The point was to create an obvious point for another refactor, but since that aesthetic has become a problem rather than a help, I'll simply roll it back to "create".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No idea what you are thinking of here. Specifics? As part of the constructor injection work I started, I actually moved the creation (ok, ok.. registration) of Configurations way earlier. And it worked utterly fine. I have this:
I was trying to limit the size and scope of the PR. As it stands, the bulk of the logic is not changed, and simply moved to a new location that makes it obvious how it can be further improved. In order for me to break the creation away from the fetching of these Configurations, I'd have to make design choices, which could further mire the PR in debate.
I can very clearly move these "setUp*Configuration" function bodies to another spot, but then PlatformImports will need some thought. They're tightly coupled and created per project, which is its own issue as well. Possibly, in addition to being moved, the logic that creates PlatformImports should be rewritten to run later, or using different metadata.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think I'm holding this up. At least I hope not. I don't think it should.
You asked a question, I answered and we had a discussion ;) I totally agree its best to do incremental, targeted pull requests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I misunderstood your intention. I thought you were asking for changes. I was being defensive because the result of this conversation, if I tried to address your questions with code, would have resulted in a lot of work. At that point I'd probably turn the PR into a draft, or close it and start another one, to go over ideas.
I have changed "register" to "create" as that was easy enough to do.
.../gradle-model/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java
Outdated
Show resolved
Hide resolved
@@ -167,46 +159,6 @@ private static void collectDestinationDirs(Collection<SourceDir> sources, final | |||
} | |||
} | |||
|
|||
private PlatformImports resolvePlatformImports(Project project, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found it hard for me to parse the separation of concerns between ToolingUtils, GradleApplicationModelBuilder, and ApplicationDeploymentClasspathBuilder. Hopefully this consolidation of Configuration-constructing logic into the ApplicationDeploymentClasspathBuilder makes sense and doesn't deviate from the intention.
BuildResult result = GradleRunner.create() | ||
.withProjectDir(testProjectDir.toFile()) | ||
.withArguments("publishToMavenLocal") | ||
.withPluginClasspath() | ||
.build(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is actually working around a potential bug in which the plugin code cannot resolve conditional extensions which are not published. The integration test was written against 2.8.0.Final vanilla before I did any refactoring, so the issue is present in released Quarkus. I can file an issue if this is not intended behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jskillin-idt did you notice we have tests for conditional dependencies https://github.com/quarkusio/quarkus/blob/main/integration-tests/gradle/src/test/java/io/quarkus/gradle/ConditionalDependenciesTest.java and the relevant projects under https://github.com/quarkusio/quarkus/tree/main/integration-tests/gradle/src/main/resources/conditional-dependencies
Although I would prefer them to be generated. There is a kind of framework to do that for Maven but not for Gradle yet. Anyway, what we are also doing in our integration tests is we testing against the current Quarkus repo instead of a specific platform release, e.g. 2.8.0. In all test projects gradle.properties
aren't including the plugin and the quarkus versions. Those are filled in during the test initialization/setup. You can see that in io.quarkus.gradle.QuarkusGradleTestBase.getProjectDir(projectName)
(although this looks like should be refactored).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh this is perfect! Awesome. I don't mind the tests being a little crusty as long as they get the testing done :) I didn't even think to look beyond the devtools directory. Thank you for the references.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've removed these tests as the ones you linked cover it and more.
This comment has been minimized.
This comment has been minimized.
@jskillin-idt could you please build the |
@jskillin-idt thanks, looks good! Could you please squash the commits into one? One thing I didn't mention but we did experiment with and that's related to the config resolution. You and @sebersole are probably gonna hate the idea :) There was/is an idea to add Gradle tasks, Maven goals and Quarkus CLI commands to, e.g., build and push container images. There are different ways to build an image: using docker, jib, s2i, openshift, buildpack. And for each of those ways there is a Quarkus extension [1]. A Quarkus extension is typically added by the user to the project as a regular dependency. However, there was an idea to add Gradle tasks that would automatically add the necessary image building Quarkus extension if it's not already present in the configuration as a dependency behind the scenes. For that we have a notion of "forced dependencies". Those are dependencies that can be enforced on the configuration by tasks. This hasn't been implemented/supported yet. I did have a branch I was playing with and there I had to delay the config resolution. But I realize this is pretty tricky and is probably going against the typical Gradle model and with the caching of task outcomes. Perhaps we need to look for some other approach here. But I thought I'd mention this anyway. That's actually not the only case where we have these [1] https://code.quarkus.io/?extension-search=origin:platform%20image |
I'm still getting to the point of working through the Configurations myself just to better understand various aspects of them. So I really am not the best person to comment on this. I will say however, that it sounds quite similar to what I did in the poc - essentially defining these extensions (special dependencies) via a mechanism other than A major benefit of this is not needing to resolve all dependencies for the project just to discover extensions - we are explicitly told the extension GAV and can resolve just that dependency graph, not all dependencies. Which brings us back to the discussion of "Configuration design", which goes beyond the work done here as even @jskillin-idt points out. But let's discuss that on the Discussion. Though maybe we should probably create smaller Discussions? The existing one is getting hard to follow due to how large it got. |
I think Gradle is flexible enough to do pretty much whatever you guys want. I think if you can organize your... I guess I'll call it "input" metadata... during the configuration phase (which Steve has started to actually put to code, whereas I've only hinted at it here) and then use that to "output" metadata about the build environment, and have tasks use that to basically just do the thing that's already been decided on, then that would fix the issues you're anticipating around caching, parallel execution, and in my case, dependency management. |
I will take a look at the referenced code and let you know in the GitHub discussion if I have any related ideas. |
I wanted to mention... My poc uses a NamedObjectContainer with DSL for defining the extensions to use. But simply using dedicated Configurations could have the same effect - specifically one for platforms and another for extensions. E.g.
It is the fact that we can handle platforms and extensions separately from "normal" dependencies that is useful. |
I started #25665 to discuss the design of Configurations related to applying extensions |
This allows Gradle to natively access the dependency trees that Quarkus' tasks use, which means that Gradle's analysis tools should work properly when dealing with Quarkus' dependencies.
I've rebased and squashed, ran the "quickly" build and the Gradle integration tests. Do I just force push to my forked branch to finish? |
Yes, please! |
The purpose of this PR is to demonstrate a possible way in which the creation of Configuration objects can happen during the configuration phase, and then how the population of Configurations can be delayed until they are specifically requested by the prepare/build/generate tasks later. This allows Gradle to natively access the dependency trees that Quarkus' tasks use, which means that Gradle's analysis tools should work properly when dealing with Quarkus' dependencies.
The refactor I attempted on ApplicationDeploymentClasspathBuilder.java, in which I unified all the logic concerning how the platforms and extensions are discovered, also revealed the circular dependency between the runtime and deployment configurations. The deployment configuration must reference the runtime configuration to discover which runtime extensions are present which it then uses to discover both the deployment extensions, and any conditional runtime extension dependencies. The runtime configuration must ask the deployment configuration to resolve before it is usable, because the deployment configuration may find new runtime extensions to add. I believed that this PR was already sizeable enough so I have left this for another day.
This PR does change the Gradle API in a minor way:
quarkusExtension { dependencyCondition = [] }
was renamed toquarkusExtension { dependencyConditions = [] }
since the grammar is more accurate, and the internal code already used the plural form. This can be undone if desired.This PR otherwise should not change the end user interaction with the existing plugins. It should, however, mean that "gradle dependencies" will produce accurate, up-to-date dependency trees as Quarkus' tasks will view them, which enables "dependencyInsights" (and possibly build scans; I don't have access to them) to also properly assist with dependency conflicts, if Gradle's stricter dependency features have been enabled.
This PR is likely lacking proper testing. I attempted to write an integration test to cover the basics of conditional dependencies, as they were the most likely to break. I am uncertain as to how to fully test these changes to ensure nothing else broke. I'm willing to write more tests once I know what I am looking for. The integration test I wrote is also possibly not of the quality or style that the Quarkus project wants. The tests themselves are simple enough, so I could rewrite them if, again, I know what I am targeting.