I have a multi-module project, one of the module b...
# plugin-development
c
I have a multi-module project, one of the module being a custom plugin. This plugin shall be visible to its own module test source code (to be able to apply it on a test project obtained by
org.gradle.testfixtures.ProjectBuilder().build()
), and to other test-only modules. Using the
buildSrc
strategy or the
includeBuild
strategy for the custom plugin module itself are not suitable for this plugin module since it does split the build configuration and make it much harder to mutualize configuration, resources, versions, etc. So I am expecting the custom module plugin to be a standard module among the others, in the same build unit. But the tests modules could be somehow separated, I guess. What are the applicable build architectures for defining, testing and applying a custom plugin that would not isolate the custom plugin module in a separate build context, yet have the custom plugin be visible to tests modules, and to its own tests source code?
v
Should that plugin also be applied to any of the projects in the build that is building the plugin, or is that plugin only for consumption by other builds?
c
It should be applied to the module(s) under the
examples
folder (and to the test project in its own tests, but its tests could be moved elsewhere if that's more convenient).
t
Those
examples
could be their own build (rather than subprojects in your main build) that
includeBuild
the parent folder.
c
Then that would mean that they cannot be used as functional tests for the plugin itself, except if there is a way to launch the examples build from within the main build.
t
Run them with TestKit? or possibly more appropriate: Examplar https://blog.gradle.org/documentation-samples-testing-exemplar
šŸ‘€ 1
šŸ‘† 1
c
That seem rather complex for the task at hand... The easiest way would be to be able to register and apply the plugin from its build directory. Isn't there any way of doing so?
v
As @Thomas Broyer said, for functional tests you would use TestKit usually. If you just want those separate builds that use it, you probably have to use a composite build and include the plugin build from those example builds.
ProjectBuilder
tests are not so much for functional tests as many moving parts are missing, but only for unit tests. In those you can just apply the plugin, yes. To centralize versions, you can for example use version catalogs, that is the built-in way to centralize version definitions and you can use the same version catalog from the plugin build and the example builds by referencing the catalog file manually. To centralize common build logic, you would put that build logic to yet another separate build that you then include from the example builds and the main build.
c
The custom plugin is intended to be published as a full citizen artifact, it's not only for internal use. What I don't understand is that once the custom plugin is built, I have a jar for it, so why isn't there any simple solution where I would register the plugin from its jar in the examples modules? What is blocking?
v
Your build is first configured, needing all plugins. Then the tasks are executed. If the same build builds the plugin and needs to consume the plugin, you have the classical chicken-and-egg situation.
c
Ok, that's very clearly why we need two builds, thank you. Nevertheless, the solution is not yet very clear to me. I have a functional multi-module project with, say:
Copy code
common
core (-> common)
api-client (-> common)
api-server (-> core)
plugin (-> core)
And if I want to add a
test
module to it using the plugin, switching to a multi-project build brings a complete havoc to the structure, with test having to be a separate build, and what... including the build of the root project?! Having all modules being independant builds?! What about the incidences on dependencies, configuration, maven-publish, etc. It looks really weird. There should be some way to explain to gradle at the configuration step that a specific module uses a plugin yet to be produced, or to somehow serialize the builds... Of course, I can always have an entirely separated example project. The release process would then be; • publish the main project on maven local • check the build on the example project (the main point being that the example project, as well as providing examples to users, is heavily used in the plugin test cases) • if it looks ok, publish the main project There should be some way to internalize this process in gradle. For now, I will use this manual workaround.
t
If the goal is to exercise the plugin in tests, then having separate builds is the way to go; and the main build then runs those other builds (but does not include them), with TestKit or Examplar or something similar. So
./gradlew build
will both build the plugin and test it through other projects that apply it. What you do want is to have a single command to both build the plugin and run functional tests, not necessarily have a single build that both builds and applies the plugin. https://docs.gradle.org/current/userguide/testing_gradle_plugins.html#functional-tests
šŸ‘€ 1
I haven't tried it but you could probably even have your example projets be totally independent from the parent build, declaring dependencies on published artifacts; then the functional tests could run them with
--include-build ${projectPath}
(instead of a hard-coded
includeBuild("../")
in the settings script)
c
the main build then runs those other builds (but does not include them), with TestKit or Examplar or something similar.
Who does the publishing, then?
v
Let's start with clearing up language to be on the same side. In Gradle there are no "modules". What you call "module" is a project". What you call "multi-module project" is a "multi-project build". What you call "multi-project build" is a "composite build".
switching to a multi-project build brings a complete havoc to the structure,
What do you mean by that? You just define a new project
test
, in which you include the other build with all the projects within
pluginManagement { ... }
and can then just apply the plugin to your test project like normal.
Having all modules being independant builds?!
You can do that, but there is no reason to do so. That's purely a question of how you want to structure your build.
What about the incidences on dependencies, configuration, maven-publish, etc. It looks really weird.
I have no idea what "incidences" you talk about or what you might think "looks really weird" unless you more concretely describe your thoughts.
There should be some way to explain to gradle at the configuration step that a specific module uses a plugin yet to be produced, or to somehow serialize the builds...
Sure, and you were just told the way. Composite build is exactly what you ask for. Within one build, no, there should not be such a configuration option, that would be very unclear, unmaintainable, fragile, and totally unnecessary complex when there already exists a proper and better way. :-)
Of course, I can always have an entirely separated example project. The release process would then be;
• publish the main project on maven local • check the build on the example project (the main point being that the example project, as well as providing examples to users, is heavily used in the plugin test cases) • if it looks ok, publish the main project
There should be some way to internalize this process in gradle. For now, I will use this manual workaround.
Well, whatever works for you, you have to live with the consequences. But a composite build is exactly what you are after. But if you refuse to use it and want to make your life much harder than it needs to be. šŸ¤·ā€ā™‚ļø That's your decision. šŸ™‚ But at least if you use that approach, don't use Maven Local, but a dedicated local repository, or at least have it last in the list of repositories and always use a repository content filter. Maven Local is broken by design in Maven already, and it will make your builds slow, fragile, and flaky if used lightly. https://docs.gradle.org/8.9/userguide/declaring_repositories.html#sec:case-for-maven-local
c
Thanks for your explanation and clarification effort... Let's be clear, I'm just trying to make it easier, not to refuse the apparently good solution, and sorry if I'm still missing obvious things, but I still don't get the big picture... So ok, let say
test
has its own build. You are suggesting that
test
should reference the other projects in its
pluginManagement
section but: • does it mean that this section will have
ncludeBuild
directives, hence having
test
depends on the whole sources of the other projects? Do those other projects have to be removed from the main build, which would then just do an
includeBuild
on test? If so, that's what I find a bit weird, as I'm expecting
test
to only require a binary dependency on the plugin and other modules. • or does it mean that
test
will use Testkit in its functional tests (as we would do in the plugin unit tests) and that it will allow this binary dependency? I'm still kindof in the dark...
t
What I would do: 1. Keep your common/core/api-client/api-server/plugin build exactly the same (for now). 2. Create a
test
or
example
or whatever folder and create a separate build in there that uses your plugin (you can plan on using a published binary, in which case include a version number). You should be able to use
cd test && ./gradlew build --include-build=..
and it would then build the plugin (and core, as it's a dependency; only the required tasks to produces the classes and resources) project before adding them to the classloader of the test/example build (if you included a version number for the plugin, you could just build without the
--include-build
and it would resolve it from the repository; otherwise, I would then add
pluginManagement { includeBuild("..") }
to the
test/settings.gradle.kts
to always use the parent build, and never a published artifact). 3. in your
plugin
project, create a functional test suite that runs the
test
project using TestKit; or use Examplar to do the same; possibly passing
--include-build=${path}
if needed). Yes, it means that running the functional tests for the plugin will actually rebuild the plugin from an included build of a forked Gradle build! (also note that this functional test suite won't actually have a dependency on the plugin itself, we just co-locate it in the plugin project because that still represents tests of the plugin) With that setup: •
./gradlew check
(
./gradlew :plugin:functionalTest
) at the root will exercise the plugin by actually running another Gradle build of the
test
build/project •
cd test && ./gradlew build
will run the test project, using the plugin; if you added an
includeBuild("..")
in the settings script, then it'll compile the plugin and core projects from the root build to use them; otherwise it'll use a published binary (and you could add
--include-build=..
on the command line to use the root build instead). Without the
includeBuild("..")
in the settings script, this really serves as an example build that can be moved out of the code base and used as-is independently. That's how I would do it, from what I understood of the information you shared.
c
Thanks a lot, I'm very grateful for your help. I'm still at step 2,
pluginManagement { includeBuild("..") }
in the to the
test/settings.gradle.kts
does the trick for the configuration step, the next problem is that the test build does also depend on :common, adding another
includeBuild("..")
outside of the
pluginManagement
section does not seem to work (the project
:common
is not found, neither is
:examples:common
). Still searching...
And instead of having those two
includeBuild("..")
directives, would including the root project in the
buildSrc
of the examples build work?
The workaround I found is to symlink needed root projects inside
examples
...
t
You need to depend on the common project using its groupId:artifactId coordinates and let project substitution of included build do its work (and if you also include a version to the coordinates, the project could possibly be run independently of the root build, without any included build, as a truly independent example project; same reasoning as for the plugin): https://docs.gradle.org/current/userguide/composite_builds.html#included_build_declaring_substitutions AFAIK you cannot use
project(…)
dependencies to reference projects of the included build from the including build, you need to rely on dependency substitution.
šŸ‘† 1
c
Thanks a lot. I'm fighting with some totally unrelated packaging issue, but your suggestions make a lot of sense and seem to work very well.
So now to step 3, running the examples build from the plugin tests with Testkit:
Copy code
package com.republicate.skorm

import org.gradle.testkit.runner.GradleRunner
import org.junit.Test
import java.io.File

class ExamplesTest {
    @Test
    fun bookshelfTest() {
        val projectDir = File("../examples")
        GradleRunner.create()
            .withProjectDir(projectDir)
            .withDebug(true)
            // .withArguments("build")
            // .withArguments(":bookshelf:build")
            .forwardOutput()
            .build()
    }
}
No matter what, no error is reported, and the html report is empty...
Everything worked as expected in the end. Thank you both again for your help.
šŸ‘Œ 1